一看就懂的OpenGL ES教程——仿抖音滤镜的奇技淫巧之变换滤镜(理论基础篇)

2,100 阅读17分钟

通过阅读本文,你将获得以下收获:
1.图像变换的概念
2.图像变换的数学原理:向量、矩阵
3.通过矩阵对图像进行变换操作

唠嗑几句

时光荏苒,本专栏由于众所周知和众人所不周知的原因已经停更好长一段时间了,在这段时间里,发生了很多事情。卡塔尔世界杯圆满落下帷幕,球王梅西终于真正加冕,从此真正封王(封面致敬)。国内疫情在封控了长达3年时间之后,终于放开,经过一个月左右非常艰难的全国个人抗疫时期之后,现在疫情的阴云终于散去,如今生活暂时看起来已经逐步恢复到疫情前的模样,希望可以从此作别疫情。现在新年已经拉开序幕,回来上班转眼已经2个星期,似乎已经没有理由再蓄势待发(偷懒躺平)了,到了继续专栏脚步的时候了,于是我便打开了笔记本开始奋笔疾书…

8f54ec5fc8d908322d28cae731185b81.jpeg

上篇回顾

上一篇文章一看就懂的OpenGL ES教程——仿抖音滤镜的各种奇技淫巧之基础滤镜 已经给大家展示了如何使用OpenGl es实现最基本的滤镜,包括灰度滤镜、反色滤镜、分镜滤镜,很多童鞋表示看得还不够过瘾,表示要看更炫的滤镜,上一篇滤镜讲到黄金段位,那么我们依照王者的段位继续下一种滤镜:变换滤镜,从这里开始,滤镜开始迎来动画的元素

既然是变换滤镜,那么有一个基础知识真的是重中之重,它事关各位学习图像处理相关的前途,这个重要的知识点就是——图像的变换。所谓”勿在浮沙筑高台“,今天就来好好打打数学基础。(有些童鞋看到数学可能会有点慌,但是请莫慌,请相信我能讲的通俗易懂~)

588b7277463a26551303106b9e64e0d6.jpeg

图像变换

这里的变换具体指的是几何变换,变换分为2大类:

一种是仿射变换,比如平移、缩放、旋转、错切这些常见的2维操作以及其组合操作,他们的特点是原来平行的直线变换之后依然保持平行

另外一种是透视变换,它是一种3维空间的变换,特点是原来平行的直线变换之后不一定保持平行,一般用于图像的矫正(例如在移动机器人视觉导航研究中,由于摄像机与地面之间有一倾斜角,而不是直接垂直朝下(正投影),有时希望将图象校正成正投影的形式)。

当然今天的主题是仿射变换,关于透视变换可能会在以后的图像处理相关博文讲到,大家可以拭目以待一下哈哈。

图像的变换其实本质就是图像上每个点位置的变化,我们知道,空间坐标系中的每个点都可以用(x,y,z)这样的坐标点来表示:

坐标系.png

如果看过之前专栏的章节,就知道在图像中每个点在OpenGL的是用向量vec来表示的,大家如果已经写过几次着色器代码,那应该对于vec这东西很熟悉了吧,所以对于点的变换,就是对向量的变换。那么,今天就必须先来一堂数学课,来从向量入手,慢慢揭开图像变换的面纱。

向量基础知识

其实这个是高中的数学知识了,就当做复习一下,所谓温故而知新,可以为师矣~

4e6ebc087ec85285899c26725c3c5fdb.jpeg

我们平常经常接触的类似1+1的运算就是标量的运算,标量(Scalar)只是一个数字。而向量有一个方向(Direction)和大小(Magnitude,也叫做强度或长度)的数字,比如在2D空间中,向量就长得以下的模样:

image.png

上图表示有3个向量v\overrightarrow{v}w\overrightarrow{w}n\overrightarrow{n}。我们通常设定一个向量的原点为(0, 0),然后是指向一个方向的一条线段。比如说向量ab\overrightarrow{ab}是起点在(0, 0),并会指向(a, b)的一条线段。2维向量可以看做是在3维空间坐标系中z分量为0的情况

在图像处理领域,我们一般用向量来表示点的位置,通过向量的计算来处理对点的变换操作。

向量的运算

向量与标量运算

当把一个向量加/减/乘/除一个标量,我们可以简单的把向量的每个分量分别进行该运算,例如向量v\overleftarrow{v} = (2,4),那么v\overleftarrow{v}/2 = (1,2)。不过有一点要注意,就是减法除法向量只能作为被减数或者被除数,如果倒过来让标量减/除以向量,那是没有任何意义的。

向量加法

因为向量是带有方向的,所以运算的时候这个方向就必须考虑进去,一图胜千言,向量加法如下图所示:

image.png

上图是向量v\overrightarrow{v}和向量k\overrightarrow{k}相加,可以这样子通俗易懂地理解向量相加:一个人从原点O出发,先走到(4,2),然后转弯,以(4,2)为临时原点走到(1,2)点,这样,他就走到了原点O坐标系下的(5,4)点,即向量(4,2)+(1,2) = (5,4),效果和从原点直接走到(5,4)点是一样的。所以向量加法得到的结果就是相加的向量头尾连接在一起的结果

当然也可以把第二个向量平移到原点,如下图:

image.png

那么相加后得到的向量其实就是2个向量组成的平行四边形的对角线,方向由原点指向2个向量的平行辅助线的交点

可以看出,向量加法其实就是将各个分量叠加起来,即如果向量v\overrightarrow{v}=(a,b),向量u\overrightarrow{u}=(c,d),则v\overrightarrow{v}+u\overrightarrow{u}=(a+c,b+d)。

向量减法

减法本质也是加法,只是加上向量和一个负数标量的乘积,比如向量w\overrightarrow{w}-v\overrightarrow{v}
w\overrightarrow{w}+(-v\overrightarrow{v}):

image.png

由图可以看出,向量相减得到的结果就是从作为减数的向量的箭头指向作为被减数的向量的箭头的新的向量

向量乘法

向量的乘法内容就开始有点丰富了,有2种玩法,分别是点乘叉乘

点乘:

两个向量的点乘等于它们各个分量相乘结果之和。即如果向量v\overrightarrow{v}=(a,b),向量u\overrightarrow{u}=(c,d),则v\overrightarrow{v}.u\overrightarrow{u}=(ac+bd)。

从几何意义上来说,2个向量点乘等于2个向量的长度乘上其夹角的余弦值

v\overrightarrow{v}.u\overrightarrow{u} =vucosθ|\overrightarrow{v}||\overrightarrow{u}|\cos\theta

其中v|\overrightarrow{v}|可以通过勾股定理来得:

image.png

所以: v=a2+b2|\overrightarrow{v}|=\sqrt{\def\foo{a^2} \foo + \def\foo{b^2} \foo}

点乘的几何意义是可以用来表征或计算两个向量之间的夹角,以及在b向量在a向量方向上的投影.

叉乘:

点乘得到的是一个标量,叉乘得到的依然是一个向量。 叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个垂直于两个输入向量所在平面的第三个向量。如果输入的两个向量也是正交的,那么叉乘之后将会产生3个互相正交的向量。

如下图所示,v\overrightarrow{v}k\overrightarrow{k}的叉乘结果为垂直于它们的第三个向量。

image.png

如果向量v\overrightarrow{v}=(a,b,c),向量k\overrightarrow{k}=(d,f,g),则两者的叉乘v\overrightarrow{v}×w\overrightarrow{w}为:

(abc)\begin{pmatrix}a\\b\\c\end{pmatrix}×(dfg)\begin{pmatrix}d\\f\\g\end{pmatrix}= (bgcfcdagafbd)\begin{pmatrix} bg-cf\\ cd-ag \\ af-bd \end{pmatrix}

老实说,还是有点难记的,不过确实没必要专门记住,只要记得叉乘结果为垂直于它们的第三个向量即可~

说了半天,叉乘有什么作用呢?

在3D图像学中,叉乘的概念非常有用,可以通过两个向量的叉乘,生成第三个垂直于a,b所在平面的法向量。(关于法向量有什么作用,敬请期待后面章节的讲解。)

57cce9442ff8213c257bb96f54d5a702.gif

矩阵基础知识

向量的基础已经带各位回顾完,接下来开始回顾矩阵了。

简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。比如下图就是一个2*3的矩阵:

[236598]\begin{bmatrix}2 & 3 & 6\\ 5 & 9 & 8 \end{bmatrix}

这里每个元素有它的索引,就是一个记录元素在矩阵中的位置的标记,比如上面的矩阵,元素“2”索引为(0,0),元素“9”索引为(1,1),以此类推。

表面看矩阵就是一个由多个数字组合起来的方阵,但是它对于搞图形处理来说,那可是意义重大,就犹如经典日漫《钢铁神兵》中铁兵得到了X一样(没看过这个动漫?强烈推荐去看),下面就一步步向你揭晓它在图像处理中的作用。

首先,我们先了解下矩阵的基本运算:

矩阵加减法

矩阵加减法很简单,就是对相同索引下的元素进行加减,那就是说只有行数和列数一致的矩阵才能进行加上运算。也就是说一个3×2矩阵和一个2×3矩阵(或一个3×3矩阵与4×4矩阵)是不能进行加减的。

矩阵相加减分别如下面2个图所示:

image.png

image.png

简单到没什么好讲的~

矩阵乘法数乘和

和向量一样,矩阵乘法又分为数乘和矩阵之间相乘。

矩阵数乘

数乘就是矩阵和一个标量进行乘法运算,矩阵与标量之间的乘法也是矩阵的每一个元素分别乘以该标量,如下图所示:

image.png

好像又是简单到没什么好讲的…

矩阵相乘

说到矩阵与矩阵的乘法,刚接触的时候总觉得有点让人觉得别扭,2个矩阵相乘,简单来说,就是第一个矩阵的行与第二个矩阵对应的列的元素一一相乘之和作为结果矩阵的对应元素…

看起来有点抽象吧,我们举个例子就能看懂了。

以下是2个矩阵的相乘:

image.png

可以看到左边2个红框的每个元素对应相乘再相加就能得到右边红框部分对应的元素,结合我上面总结的,可能大家都能看懂了。那么总结下通用的公式即:

结果矩阵的第(i,j)是第一个矩阵的第i行和第j列的所有对应元素(即所有的第一个矩阵的(i,n)元素和第二个矩阵的(n,j)元素)相乘,并将所有相乘相加而成。

于是又得到了两条结论:
1.只有当左侧矩阵的列数与右侧矩阵的行数相等,两个矩阵才能相乘。
2.结果矩阵的维度是(n, m),n等于左侧矩阵的行数,m等于右侧矩阵的列数。
3.矩阵乘法不满足交换律,满足结合律(这个各位可以亲自证明下)

图像变换矩阵

上面章节讲过向量一般用来描述空间中的一个点,那么矩阵可以说就是用来处理这个点的一个设计精巧的工具装备

缩放变换

为了简单理解,这里从二维空间切入。这里假设二维空间中有一个时钟,现在要把时钟缩小到原来的0.5倍大小,如下图:

image.png

要怎么处理呢?当然聪明的你,很快能想到,就是对时钟的每个点对应的向量做缩小的计算

假设时钟上的一个点坐标为(x,y),现在要对该点进行缩小0.5的计算,即将该点挪动到(0.5x,0.5y)的地方,假设该点为(x',y'),则可以得到下面的方程式子:

{   x=0.5x   y=0.5y\begin{cases}    x' = 0.5x\\    y' = 0.5y \end{cases}

前面我们说到空间中的点可以用向量来表示,那么聪明的古人就思考,能不能把上面的方程式子拆成三个部分呢:

第一部分,代数x左边的系数;

第二部分,代数x本身;

第三部分,是等号右边的数字

于是聪明的古人将上面的方程式子转化为这样的表示形式:

[xy]\begin{bmatrix}x' \\y' \end{bmatrix} = [0.5000.5]\begin{bmatrix}0.5 & 0 \\ 0 & 0.5 \end{bmatrix} [xy]\begin{bmatrix}x \\y \end{bmatrix}

咦,最左边的不就是矩阵么?

所以这样拆出来的好处是对图像上的点的变换处理,就可以用矩阵和向量相乘的形式表示了。

于是假如x方向缩放系数为a,y方向的缩放系数为b,则我们可以得到一个缩放的通用变换矩阵:

[a00b]\begin{bmatrix}a & 0 \\ 0 & b \end{bmatrix}

而在后面章节,我们更能体会这种变换矩阵的好处。

另外提个有意思的东西,当a或b为负数的时候,就可以完成镜面翻转变换

当a为负数时,就是对y轴做镜面翻转: image.png

当b为负数时,就是对x轴做镜面翻转: image.png

0fa67f93d559f02dca5809fc26b06b9e.jpg

平移变换

再看下平移的例子: image.png

如上图所示,就是将空间中一个矩形向x,y方向分别移动(-xlx_l,-yly_l)。

我们又可以假设矩形中的一个点为(x,y),在x,y方向分别平移xtx_tyty_t到点(x',y')则可以得到以下方程式子:

{   x=x+xt   y=y+yt\begin{cases}    x' = x + x_t \\    y' = y + y_t \end{cases}

于是聪明的你一定能推到为向量的表示形式:

[xy]\begin{bmatrix}x' \\y' \end{bmatrix} = [xy]\begin{bmatrix}x \\y \end{bmatrix} + [xtyt]\begin{bmatrix}x_t \\y_t \end{bmatrix}

就是这么简单,连矩阵都不需要~

旋转变换

变换越来越有意思了,一起来看下旋转变换:

image.png

旋转的变换矩阵的推导就没有缩放和平移来的那么简单了。以下是让人热血沸腾的数学推导过程,不感兴趣的同学可以直接看最后的结论。

我们依然对其中某个点作为分析的切入点:

假设某个点为a,此时要将a逆时针旋转ϕ\phi角度到b点,如下图所示:

image.png

数学推导过程:
第一步,列出点a坐标点和其对应向量长度以及向量和x轴夹角之间的关系:
假设a点的坐标为(xax_a,yay_a),则a点对应的向量a\overrightarrow{a}的长度r为:

r=xa2+ya2r=\sqrt{\def\foo{x_a^2} \foo + \def\foo{y_a^2} \foo}

假设a和x坐标正方向的夹角为α\alpha,则可以推导出:

xa=rcosαx_a = r\cos\alpha
ya=rsinαy_a = r\sin\alpha

第二步,列出点b坐标点和其对应向量长度以及向量和x轴夹角之间的关系:
因为b点是由a点旋转得到的,所以对应的向量b\overrightarrow{b}的长度也是r,不同的是向量b\overrightarrow{b}和x轴正方向的夹角为(α+ϕ\alpha+\phi),于是同样可以推出:

xb=rcos(α+ϕ)x_b = r\cos(\alpha+\phi)
yb=rsin(α+ϕ)y_b = r\sin(\alpha+\phi)

可能不少人就卡到这里了,那么接下来要怎么继续推导呢?(接下来就是体现多年的数学功底的时候了)

439e1962b1958c39a710e5d0282151fd.jpg

三角函数中,有个东西叫做二角和差公式,于是通过这个公式,又可以将上面的式子拆解为:

xb=rcosαcosϕrsinαsinϕx_b = r\cos\alpha\cos\phi - r\sin\alpha\sin\phi
yb=rsinαcosϕ+rcosαsinϕy_b = r\sin\alpha\cos\phi + r\cos\alpha\sin\phi

第三步,将第一步得到的关系式子代入第二步得到的关系式子:

xb=xacosϕyasinϕx_b = x_a\cos\phi - y_a\sin\phi
yb=yacosϕ+xasinϕy_b = y_a\cos\phi + x_a\sin\phi

第四步,将第三步转化为向量和矩阵之间的关系式子:

走到第三步,根据之前缩放的讲解,很多童鞋应该可以推导出变换矩阵了吧:

[xbyb]\begin{bmatrix}x_b \\y_b \end{bmatrix} = [cosϕsinϕsinϕcosϕ]\begin{bmatrix}\cos\phi & -\sin\phi \\ \sin\phi & cos\phi \end{bmatrix} [xaya]\begin{bmatrix}x_a \\y_a \end{bmatrix}

于是,可以大声宣布,如果要对一个图像逆时针旋转ϕ\phi 度,则旋转变换的变换矩阵就是

[cosϕsinϕsinϕcosϕ]\begin{bmatrix}\cos\phi & -\sin\phi \\ \sin\phi & cos\phi \end{bmatrix}

c201dc42302827a26119b55c4b642119.jpeg

组合变换

通过上面的讲解,我们知道原来对一个图像的做几何变换,可能要对每个点进行多个步骤的代数和三角函数,现在变成用一个矩阵就能解决问题了,而现在计算机gpu对于矩阵计算可是非常擅长的,这样子不仅人看起来舒服方便,计算机也非常爽。不过,上面不同的变换,所运用的矩阵也是不同的,能不能将其大一统,即用一个矩阵来表示多个变换呢?即用以下的表示方式表示各种变换的组合

[xy]\begin{bmatrix}x' \\y' \end{bmatrix} = [abcd]\begin{bmatrix}a & b \\ c & d \end{bmatrix} [xy]\begin{bmatrix}x \\y \end{bmatrix}

比如我们现在要对一个点(x,y)先进行缩放(x轴缩放a倍,y轴缩放b倍),然后进行逆时针旋转ϕ\phi度,最后再进行平移(x轴平移x_t,y轴平移y_t),根据上面的每种变换推出来的结论,假如变换后的点位(x',y')我们可以得到式子(这里要注意相乘的顺序,因为矩阵不满足交换律,所以先变换的要先和点向量相乘):

[xy]\begin{bmatrix}x' \\y' \end{bmatrix} = [cosϕsinϕsinϕcosϕ]\begin{bmatrix}\cos\phi & -\sin\phi \\ \sin\phi & cos\phi \end{bmatrix} [a00b]\begin{bmatrix}a & 0 \\ 0 & b \end{bmatrix} [xaya]\begin{bmatrix}x_a \\y_a \end{bmatrix} +[xtyt]\begin{bmatrix}x_t \\y_t \end{bmatrix}

缩放矩阵和旋转矩阵的相乘最后得到的还是一个矩阵,所以还是线性变换,问题就出现”万恶“的平移,是加一个固定向量,导致整个式子不是线性变换,以至于不能用一个矩阵便是所有变换的组合,而我们又不想平移变换成为一个特例。

聪明的古人早已巧妙解决了这个问题,他们用的解决方案叫做齐次坐标

6c2605a5f19f32f1a8400ac22534e7be.png

齐次坐标

用二维的方式的确解决不了这个问题,那么把思路打开,可不可以用3维矩阵的方式来解决呢?我们可以尝试拓展一个维度看看:

比如现在假设点的向量变为为[xy1]\begin{bmatrix}x \\y \\1 \end{bmatrix},即拓展的多一维固定指定为1,那么最后变换结果的点向量就会变成:[x+xty+yt1]\begin{bmatrix}x'+x_t \\y'+y_t \\1 \end{bmatrix},于是,就可以找到一个神奇的矩阵可以表达平移变换了:

[10xt01yt001]\begin{bmatrix}1 & 0 & x_t \\ 0 & 1 & y_t \\ 0 & 0 & 1 \end{bmatrix}

通过巧妙增加一维,再利用矩阵乘法的法则,使得用矩阵乘法也可以表示一个向量的相加。

[xy]\begin{bmatrix}x' \\y' \end{bmatrix} = [xy]\begin{bmatrix}x \\y \end{bmatrix} + [xtyt]\begin{bmatrix}x_t \\y_t \end{bmatrix}

\Rightarrow

[x+xty+yt1]\begin{bmatrix}x'+x_t \\y'+y_t \\1 \end{bmatrix} = [10xt01yt001]\begin{bmatrix}1 & 0 & x_t \\ 0 & 1 & y_t \\ 0 & 0 & 1 \end{bmatrix} [xy1]\begin{bmatrix}x \\y \\1 \end{bmatrix}

而增加的这一维,就是齐次坐标。

以此类推,可以推出在增加齐次坐标之后,各种变换对应的变换矩阵如下所示: image.png

于是乎,组合的变换就可以用大一统的矩阵表示了

[xy1]\begin{bmatrix}x' \\y' \\1\end{bmatrix} = [10xt01yt001]\begin{bmatrix}1 & 0 & x_t \\ 0 & 1 & y_t \\ 0 & 0 & 1 \end{bmatrix} [cosϕsinϕ0sinϕcosϕ0001]\begin{bmatrix}\cos\phi & -\sin\phi & 0\\ \sin\phi & cos\phi & 0 \\ 0 & 0 & 1 \end{bmatrix} [a000b0001]\begin{bmatrix}a & 0 & 0\\ 0 & b & 0 \\ 0 & 0 & 1 \end{bmatrix} [xaya1]\begin{bmatrix}x_a \\y_a \\1 \end{bmatrix}

而最后,我们将得到类似这样的式子:

image.png

其中组合变换矩阵

image.png
即为[10xt01yt001]\begin{bmatrix}1 & 0 & x_t \\ 0 & 1 & y_t \\ 0 & 0 & 1 \end{bmatrix} [cosϕsinϕ0sinϕcosϕ0001]\begin{bmatrix}\cos\phi & -\sin\phi & 0\\ \sin\phi & cos\phi & 0 \\ 0 & 0 & 1 \end{bmatrix} [a000b0001]\begin{bmatrix}a & 0 & 0\\ 0 & b & 0 \\ 0 & 0 & 1 \end{bmatrix} 的计算结果。

其中mxm_x表示缩放、旋转、错切相关的变换,xtx_tyty_tztz_t表示平移变换。

由于不同变换的顺序不同,最后得到的mkm_k也是不同。我们可以看下面的图体会下不同变换顺序对结果的影响:

image.png

以上分别是对同一个图像先进行缩小再旋转与先旋转再缩小操作,可见结果是不一样的,因为前面一步的操作会影响后面的操作,这也和矩阵不符合交换律一致

这种整合起来的变换就是前面已经讲过的仿射变换(线性变换加平移)。

我们要记住的就是,对于仿射变换来说,变换矩阵最后一行是固定的[0,0,1]:

image.png

投影变换就不是固定的[0,0,1]

当然其次坐标的作用还不止这些,在3D空间中,它还表示和深度相关,在投影变换矩阵的计算中十分重要

总结

今天妥妥给各位讲了一节数学课,不知各位看明白了没(不明白的可能继续看下一篇就明白了哈哈)。主要是回顾了向量、矩阵的基础知识,并重点讲了图像处理中变换矩阵的推导,最后讲了齐次坐标的概念,并阐述了如何通过齐次坐标来解决平移不能“大一统”到组合变换矩阵的问题。下一篇,就将开始把今天讲的理论知识一一付诸实践,这个时候,你就能真正体会到变换矩阵”大一统“的优越性,真正体验图像变换的乐趣~下一篇:一看就懂的OpenGL ES教程——仿抖音滤镜的之变换滤镜(实践篇)

项目代码

opengl-es-study-demo 不断更新中,欢迎各位来star~

参考:

GAMES101-现代计算机图形学入门-闫令琪

变换

Fundamentals of Computer Graphics, Fourth Edition

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

系列文章目录

体系化学习系列博文,请看音视频系统学习总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目 欢迎各位来star~

相关专栏:

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

一看就懂的OpenGL es教程