不少知识都是毕业了才知道为什么要学,继而有了学习的动力。想标题的时候发现连这门学科叫什么都反应不过来了……先后想起「离散数学」「高等数学」等等不忍回首的课程,最后终于憋出来正确的「线性代数」四个字。以下便是今回复习这个看了又忘忘了又看了不知道多少遍的东西的笔记。
前情提要,简称前提
左右手坐标系:手指沿 X 轴方向伸展,卷向 Y 轴正方向,大拇指的方向是 Z 轴正方向。
向量 Vector
向量是表示大小和长度的几何对象,有一个起点和一个终点。向量有一种重要的分类方法,下面会反复提到:
- 固定向量:起点和终点固定(例如表示空间中的位置)
- 自由向量:只关注大小和方向,与起点无关(例如表示作用力)
至于向量相加、相减、与标量相乘、取长度、标准化,这些都是很显而易见的操作,此处省略一万字。
点乘
向量 $\vec{a}=[a_1,a_2,\cdots,a_n]$ 和 $\vec{b}=[b_1,b_2,\cdots,b_n]$ 点乘的定义如下:
$$
\vec{a} \cdot \vec{b} = \sum_{i=1}^n a_ib_i = a_1b_1 + a_2b_2 + \cdots + a_nb_n
$$
如果 θ 是它们的夹角,那么:
$$
\vec{a} \cdot \vec{b} = \left\|\vec{a}\right\| \left\|\vec{b}\right\| \cos\theta
$$
当两个向量的长度均为 1(单位向量)时,点乘结果即 $\cos\theta$,则可以由此判断两者的位置关系(垂直、平行、……)。
叉乘
叉乘的结果是一个同时垂直于两个向量的向量,至于朝向那一边,敬请参照左手定则。
$$
\vec{w} = \vec{u} \times \vec{v} = (u_y v_z - u_z v_y, u_z v_x - u_x v_z, u_x v_y - u_y v_x)
$$
如何改变向量的参考系
对于向量 $\vec{p}$,如何将其从参考系 A 中的 $\vec{p}_A = (x,y)$ 变换到参考系 B 中的 $\vec{p}_B = (x',y')$ 呢?
自由向量
对于自由向量,我们关心的是其长度和方向,参考系改变后只要这两者没有改变就是成功。
那么我们只需要把原向量的起点拉回原点,然后把两个参考系的原点重合,用单位向量 $\vec{u}$ 和 $\vec{v}$ 在参考系 B 中表示参考系 A 的 X 轴 Y 轴正方向,那么:
$$
\vec{p}_B = (x',y') = x\vec{u} + y\vec{v}
$$
推广到 3D 环境,若 $\vec{p}_A = (x,y,z)$ 那么 $\vec{p}_B = (x',y',z') = x\vec{u} + y\vec{v} + z\vec{w}$ 其中 $\vec{u}$、$\vec{v}$、$\vec{w}$ 是 B 的坐标轴在 A 中的向量表示。
实际上 $\vec{u}$、$\vec{v}$、$\vec{w}$ 并不一定要是单位向量。如果不是单位向量,则说明这两个参考系的缩放不同。(比如同样的坐标系 A 和 B,只是一个单位是米,一个单位是厘米。)
固定向量
对于固定向量,除了长度和方向,参考系变换后向量的位置也不能发生改变。那么我们用 $\vec{O}=(x,y)$ 表示 A 的原点在 B 中的位置,则
$$
\vec{p}_B = (x',y') = \vec{O} + x\vec{u} + y\vec{v}
$$
于是推广到 3D 环境:
$$
\vec{p}_B =(x',y',z') = \vec{O} + x\vec{u} + y\vec{v} + z\vec{w}
$$
矩阵 Matrix
一个 m × n 的矩阵是一个有 m 行 n 列的矩阵,其中的元素用 ${M_i}_j$ 表示。
矩阵相乘
假设 A 是一个 m × n 的矩阵,B 是一个 n × p 的矩阵,那么它们相乘的结果是一个 m × p 的矩阵 C,其中 $C_{ij} = \vec{u}_{rowi} \vec{v}_{colj}$。
$$
\begin{aligned}
AB &= \begin{bmatrix}
-1 & 5 & -4 \\
3 & 2 & 1
\end{bmatrix} \begin{bmatrix}
2 & 1 & 0 \\
0 & -2 & 1 \\
-1 & 2 & 3
\end{bmatrix}\\
&= \begin{bmatrix}
(-1,5,-4)(2,0,-1) & (-1,5,-4)(1,-2,2) & (-1,5,-4)(0,1,3) \\
( 3,2, 1)(2,0,-1) & ( 3,2, 1)(1,-2,2) & ( 3,2, 1)(0,1,3)
\end{bmatrix} \\
&= \begin{bmatrix}
2 & -19 & -7 \\
5 & 1 & 5
\end{bmatrix}
\end{aligned}
$$
向量与矩阵相乘
向量与矩阵相乘完全可以按照矩阵与矩阵相乘的常规方法相乘,不过下面的是一种方便后续叙述的方法:
$$
\vec{u} B = [x,y,z] \begin{bmatrix}
{v_1}_1 & {v_1}_2 & {v_1}_3 \\
{v_2}_1 & {v_2}_2 & {v_2}_3 \\
{v_3}_1 & {v_3}_2 & {v_3}_3
\end{bmatrix} = x \cdot \vec{v}_{row1} + y \cdot \vec{v}_{row1} + z \vec{v}_{row2}
$$
于是很眼熟的两个推导:
$$
\vec{p} C = [x,y,z,1] \begin{bmatrix}
u_x & u_y & u_z & 0 \\
v_x & v_y & v_z & 0 \\
w_x & w_y & w_z & 0 \\
O_x & O_y & O_z & 1
\end{bmatrix} = x\vec{u} + y\vec{v} + z\vec{w} + 1\vec{O} = [x',y',z',1]
$$
$$
\vec{p} C = [x,y,z,0] \begin{bmatrix}
u_x & u_y & u_z & 0 \\
v_x & v_y & v_z & 0 \\
w_x & w_y & w_z & 0 \\
O_x & O_y & O_z & 1
\end{bmatrix} = x\vec{u} + y\vec{v} + z\vec{w} = [x',y',z',0]
$$
如果将 3D 向量 $\vec{r}=(x,y,z)$ 增扩到 4D 齐次坐标(提出者是那个莫比乌斯)$\vec{r}=(x,y,z,w)$(w = 1 表示固定向量;w = 0 表示自由向量),那么:
$$
\begin{aligned}
\vec{p}_A C &= [x,y,z,w] \begin{bmatrix}
u_x & u_y & u_z & 0 \\
v_x & v_y & v_z & 0 \\
w_x & w_y & w_z & 0 \\
O_x & O_y & O_z & 1
\end{bmatrix}\\
&= x\vec{u} + y\vec{v} + z\vec{w} + w\vec{O} \\
&= [x',y',z',w] \\
&= \vec{p}_B
\end{aligned}
$$
其中 $\vec{O}$、$\vec{u}$、$\vec{v}$、$\vec{w}$ 分别表示 B 中 A 的原点、X轴、Y轴、Z轴的齐次表示(原点表位置,补 1;坐标表方向,补 0)。于是 $\vec{p}_B=\vec{p}_AC$ 就可以进行坐标系转换了。
矩阵相乘与结合性
矩阵相乘的顺序是不能改变的,但可以改变括号。
如果有三个坐标系 F G H,矩阵 A 表示从 F 到 G 的变换,矩阵 B 表示从 G 到 H 的变换。那么矩阵 C=AB 就表示从 F 到 H 的变换。
$$
\vec{p}_F A B = \vec{p}_F (AB) = \vec{p}_H
$$
线性变换 Linear Transformations
$T(\vec{u})$ 表示对向量 $\vec{u}$ 进行的线性变换。我们可以把向量写成各坐标轴分量的形式方便后续推导:
$$
\vec{u}=(x,y,z)=x\vec{i}+y\vec{j}+z\vec{k}=x(1,0,0)+y(0,1,0)+z(0,0,1)
$$
于是一个针对向量 $\vec{u}$ 的线性变换就可以变为:
$$
T(\vec{u}) = \vec{u}A = (x,y,z)
\begin{bmatrix}T(\vec{i}) \\ T(\vec{j}) \\ T(\vec{k}) \end{bmatrix} = (x,y,z)
\begin{bmatrix}
{a_1}_1 & {a_1}_2 & {a_1}_3 \\
{a_2}_1 & {a_2}_2 & {a_2}_3 \\
{a_3}_1 & {a_3}_2 & {a_3}_3 \\
\end{bmatrix}
$$
缩放
因为缩放后的向量应该是 $S(\vec{u}) = (s_x x, s_y y, s_z z)$ 所以
$$
\begin{cases}
S(\vec{i}) = (s_x 1, s_y 0, s_z 0) = (s_x, 0, 0) \\
S(\vec{j}) = (s_x 0, s_y 1, s_z 0) = ( 0, s_y, 0) \\
S(\vec{k}) = (s_x 0, s_y 0, s_z 1) = ( 0, 0, s_z)
\end{cases}
$$
于是缩放矩阵即为 $S = \begin{bmatrix}
s_x & 0 & 0 \\\\
0 & s_y & 0 \\\\
0 & 0 & s_z
\end{bmatrix}$。
绕坐标轴旋转
将向量 $\vec{u} = (x,y,z)$ 绕 X 轴顺时针旋转 β 得到 $\vec{u'} = (x',y',z')$。
因为是左手坐标系,所以沿着旋转轴向下观察时,以顺时针方向为正角度。假设原向量旋转角度为 α,则可以得出:
$$
\begin{cases}
y = - r \sin\alpha \\
z = r \cos\alpha
\end{cases}
$$
于是将其旋转 β 后:
$$
\begin{cases}
x' = x \\
y' = -r \sin(\alpha+\beta) = -r \sin\alpha\cos\beta -r \cos\alpha\sin\beta = y \cos\beta- z \sin\beta \\
z' = r \cos(\alpha+\beta) = r \cos\alpha\cos\beta -r \sin\alpha\sin\beta = z \cos\beta + y \sin\beta
\end{cases}
$$
于是可以得出旋转函数:
$$
R_x(\vec{u}) = (x, y \cos\beta - z \sin\beta, y \sin\beta + z \cos\beta)
$$
将其应用于各个坐标轴分量上:
$$
\begin{cases}
R_x(\vec{i}) = (1, 0 \cos\alpha - 0 \sin\alpha,0 \sin\alpha + 0 \cos\alpha) = (1,0,0)\\
R_x(\vec{j}) = (0, 1 \cos\alpha - 0 \sin\alpha,1 \sin\alpha + 0 \cos\alpha) = (0,\cos\alpha,\sin\alpha)\\
R_x(\vec{k}) = (0,0\cos\alpha-1\sin\alpha, 0\sin\alpha + 1 \cos\alpha) = (0,-\sin\alpha,\cos\alpha)
\end{cases}
$$
(数学推导即是如此。但从更方便的角度看,完全省略推导,直接写出各个单位向量变换后的值是很轻松的。)
以此类推,即可得出沿着每个坐标轴旋转的变换矩阵:
$$
R_x = \begin{bmatrix}
1 & 0 & 0 \\
0 & \cos\theta & \sin\theta \\
0 & -\sin\theta & \cos\theta
\end{bmatrix}
$$
$$
R_y = \begin{bmatrix}
\cos\theta & 0 & -\sin\theta \\
0 & 1 & 0 \\
\sin\theta & 0 & \cos\theta
\end{bmatrix}
$$
$$
R_z = \begin{bmatrix}
\cos\theta & \sin\theta & 0 \\
-\sin\theta & \cos\theta & 0 \\
0 & 0 & 1
\end{bmatrix}
$$
旋转矩阵的特点是每行向量都是单位向量,而且两两垂直(即正交矩阵)。
仿射变换 Affine Transformations
简单地说,仿射变换就是线性变换+平移向量:
$$
T(\vec{u}) = \vec{u}A + \vec{b} = [x,y,z] \begin{bmatrix}
{a_1}_1 & {a_1}_2 & {a_1}_3 \\
{a_2}_1 & {a_2}_2 & {a_2}_3 \\
{a_3}_1 & {a_3}_2 & {a_3}_3
\end{bmatrix} + [b_x,b_y,b_z] = [x',y',z']
$$
将其增扩到齐次坐标(w = 1 时表示固定向量,w = 0 时表示自由向量):
$$
[x,y,z,w] \begin{bmatrix}
{a_1}_1 & {a_1}_2 & {a_1}_3 & 0 \\
{a_2}_1 & {a_2}_2 & {a_2}_3 & 0 \\
{a_3}_1 & {a_3}_2 & {a_3}_3 & 0 \\
b_x & b_y & b_z & 1 \\
\end{bmatrix} = [x',y',z',w]
$$
这里顺带提一句,HTML 5 的 canvas
里,你可以通过函数原型 setTransform(m11, m12, m21, m22, dx, dy)
看出来这一个仿射变换。
平移
平移,即保留原坐标不进行线性变换(乘以单位矩阵),只将其加上一个偏移量:
$$
T(\vec{u}) = \vec{u}
\begin{bmatrix}
1 & 0 & 0 \\
0 & 1 & 0 \\
0 & 0 & 1
\end{bmatrix} + \vec{b} =
\vec{u} + \vec{b}
$$
增扩为齐次矩阵:
$$
T = \begin{bmatrix}
1 & 0 & 0 & 0 \\
0 & 1 & 0 & 0 \\
0 & 0 & 1 & 0 \\
b_x & b_y & b_z & 1
\end{bmatrix}
$$
于是,重要的仿射变换矩阵共有五个:S T Rx Ry Rz,就是平时缩放、平移、旋转操作。
今天逛 Nico 的时候忽然起了搬运的念头,于是把原视频下载下来,开始进行传说中的「战渣浪」运动。在地图上到处逛搜集到如下信息:
- 非 FLV 格式一定会被新浪二压
- 整个文件的平均码率大于 500Kbps 会被新浪二压(也有说 1000Kbps 的)
发现不是很麻烦,懒得再去找某个版本的 MediaCoder,就直接上 FFmpeg 了……
查看文件信息
为了确定码率,首先用这样的方法查看文件信息:
ffmpeg -i <输入文件>
我从 Nico 下载视频的时候不是混杂时段,下载到的是高清 MP4 文件,于是 FFmpeg 就会输出类似这样的信息:
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'filename.mp4':
Metadata:
Duration: 00:02:59.77, start: 0.000000, bitrate: 430 kb/s
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 854x480, 315 kb/s, 30 fps, 30 tbr, 30 tbn, 60 tbc
Stream #0:1(und): Audio: aac (mp4a / 0x6134706D), 44100 Hz, stereo, s16, 111 kb/s
可以看出一共有 1 个输入文件,其中包含 1 条视频流和 1 条音频流。各自的码率已经可以看得很清楚了,这里该文件的平均码率没有超过 500Kbps,于是安心地进行下一步。
将 MP4 封装为 FLV
因为下载下来的是 MP4 格式文件,需要将文件(容器)换为 FLV 才能够不被新浪二压:
ffmpeg -i <输入文件> -vcodec copy -acodec copy -f flv <输出文件>
FFmpeg 的 -vcodec
和 -acodec
选项指定了输出文件的编码器,而填入 copy
则表示直接复制,不编码。
而 -f
选项是用来强制指定输出格式的,虽然 FFmpeg 自己会根据输出文件的扩展名猜,但还是写一下更保险。
其它未尽事宜
于是收工上传,不久发现直接成功了 = = 呃……我记得很久很久以前我用 MediaCoder 还失败了两次呢,囧。
这次很幸运的是下载到的文件码率没有超标,如果超标的话,据说这样可以指定码率:
ffmpeg -i input.mp4 -vcodec h264 -acodec aac -b:v <视频码率> -b:a <音频码率> output.mp4
至于传说中的 h264 2-pass 压制法(用来在更好的视频质量下控制码率),据说是这样的:
ffmpeg -i input.mp4 -pass 1 -vcodec h264 -an -b:v <视频码率> -f rawvideo -y NUL
ffmpeg -i input.mp4 -pass 2 -vcodec h264 -acodec aac -b:v <视频码率> -b:a <音频码率> output.mp4
这里的第一 Pass 因为目的只是取得一个包含视频信息的 log 文件,所以用 -an
禁用音频、用 NUL
(或者 /dev/null
)防止视频文件生成。
至于其它用法就留待以后要用上的时候再去研究了(懒……
以上。
背景知识
Lua 给我的感觉是:各种内置函数和标准库的存在感都是比较强的。如果执行这句:
for name in pairs(_G) do print(_G) end
就会把各种环境中已存在名称的打印出来:
- 全局变量:比如字符串
_VERSION
。
- 内置函数:比如
print
、tonumber
、dofile
之类。
- 模块名称:比如
string
、io
、coroutine
之类。
这里的全局变量 _G
就是存放环境的表(于是会有 _G
中存在着 _G._G
的递归)。
于是,平时对于全局变量的访问就可以等同于对 _G
表进行索引:
value = _G[varname] --> value = varname
_G[varname] = value --> varname = value
改变函数的环境
函数的上下文环境可以通过 setfenv(f, table)
函数改变,其中 table
是新的环境表,f
表示需要被改变环境的函数。如果 f
是数字,则将其视为堆栈层级(Stack Level),从而指明函数(1 为当前函数,2 为上一级函数):
a = 3 -- 全局变量 a
setfenv(1, {}) -- 将当前函数的环境表改为空表
print(a) -- 出错,因为当前环境表中 print 已经不存在了
没错,不仅是 a
不存在,连 print
都一块儿不存在了。如果需要引用以前的 print
则需要在新的环境表中放入线索:
a = 3
setfenv(1, { g = _G })
g.print(a) -- 输出 nil
g.print(g.a) -- 输出 3
沙盒
于是,出于安全或者改变一些内置函数行为的目的,需要在执行 Lua 代码时改变其环境时便可以使用 setfenv
函数。仅将你认为安全的函数或者新的实现加入新环境表中:
local env = {} -- 沙盒环境表,按需要添入允许的函数
function run_sandbox(code)
local func, message = loadstring(code)
if not func then return nil, message end -- 传入代码本身错误
setfenv(func, env)
return pcall(func)
end
Lua 5.2 的 _ENV 变量
Lua 5.2 中所有对全局变量 var
的访问都会在语法上翻译为 _ENV.var
。而 _ENV
本身被认为是处于当前块外的一个局部变量。(于是只要你自己定义一个名为 _ENV
的变量,就自动成为了其后代码所处的「环境」(enviroment)。另有一个「全局环境」(global enviroment)的概念,指初始的 _G
表。)
Lua 的作者之一 Roberto Ierusalimschy 同志在介绍 Lua 5.2 时说:
the new scheme, with _ENV, allows the main benefit of
setfenv with a little more than syntactic sugar.
就我的理解来说,优点就是原先虚无缥缈只能通过 setfenv
、getfenv
访问的所谓「环境」终于实体化为一个始终存在的变量 _ENV
了。
于是以下两个函数内容大致是一样的:
-- Lua 5.1
function foobar()
setfenv(1, {})
-- code here
end
-- Lua 5.2
function foobar()
local _ENV = {}
-- code here
end
而更进一步的是,5.2 中对 load
函数作出了修改。(包括但不限于 :))合并了 loadstring
功能,并可以在参数中指定所使用的环境表:
local func, message = load(code, nil, "t", env)
参考