numpy的基础操作1

参考教程

系列视频:https://www.bilibili.com/video/av11263377

本文的知识点将只包括numpy数据的基础使用方法,将会涉及到如下内容:

  1. numpy ndarry基本数组的生成
  2. 数组深浅拷贝
  3. 数组的基本形状改变
  4. 指定维度上的计算
  5. 不同shape也可以运算的操作
  6. 数组的增删操作
  7. Mask array的一些特性
  8. 小数数组打印的技巧

不过本文是属于查漏补缺性质的,重点在那些容易忽略的地方,而那些大多数人都知道的部分就不会再细写了。

linspace函数的使用(数组初始化)

之前的工作中,创建numpy的数组我一直都习惯用arange、zeros、ones这几个函数,而对linspace这个函数却不是很熟悉。linspace与arange的作用有些相似,都能生成等间隔的数组,但是两者有如下区别:

  1. 需要固定步长用arange,而需要固定生成数目的个数用linspace,因为linspace会自动计算步长,设置retstep参数可以返回计算的步长
  2. arange生成的数组不包endpoint的值,而linspace则包含
1
np.linspace(0, 15, 5, retstep=True)

view和copy(浅拷贝与深拷贝)

view时浅拷贝,copy时深拷贝,在我的一般使用中只用copy就可以满足了,不过其实view也有比较好的应用场景,如同一数组希望用不同维度不同的类型的形式使用,下面代码,arr_v2是arr_v1的view,但是在使用Ⅹ不仅将其纬度改变了还将其元素类型也改变了,对arr_v2的修改会影响arr_v1:

1
2
3
4
5
arr_v1 = np.array([1,5,10,15,20,25])
arr_v2 = arr_v1.view()
arr_v2.shape = (2,3)
arr_v2.dtype = np.int8
arr_v2[2,2] =255

修改reshape

1. reshape

shape相关的函数最常用的就是reshape了,但是注意:reshape函数是view而不是copy

2. ravel函数和flat属性

ravel将多维数组转为一个一维数组, flat属性则提供是一个迭代器顺序读取数组,使用代码如下:

1
2
3
4
5
6
# ravel
orig_arr = np.arange(12).reshape(2,2,3)
new_arr = orig_arr.ravel()
# flat
for i in orig_arr.flat:
print(i)

指定操作的维度

默认的sum、mean等操作是对数组中所有的元素进行对应的计算,如果只希望在某一个维度上或者几个维度上操作则需要设置其axis参数:

1
2
3
4
arr1 = np.arange(12).reshape((2,2,3))
print(arr1.mean())
print(arr1.mean(axis=0))
print(arr1.mean(axis=(0,1)))

shape不同的array计算

多维数组之间的运算一般都要求参与运算的array的shape要保持一致,但是也有一些运算没有这样的限制:

  1. 矩阵乘法:矩阵的乘法需要用dot函数,满足矩阵乘法的要求即可
  2. 参与运算的一个数组的shape是其中另一个数组shape的一部分
1
2
3
arr1 = np.arange(12).reshape((2,6))
arr2 = np.arange(0,6)
arr1 / arr2

添加和删减数组成员

这一部分对于我个人来说是最不熟悉的,因为在一般情况下创建新的数组然后赋值,同样也可以达到添加和删减的效果。只不过有时候赋值操作需要使用for循环来实现而无法直接完成,这个时候使用numpy数组提供的添加和删除操作就方便多了。

1. append操作

append操作原数组不变,返回的数组将变成一维,然后再后面追加元素:

1
2
orig_arr = np.arange(12).reshape(2,2,3)
ap_arr = np.append(orig_arr, [100,100])

append也可以沿着这某一个维度添加:

1
2
3
orig_arr = np.arange(12).reshape(2,2,3)
orig_arr1 = np.arange(112,124).reshape(2,2,3)
np.append(orig_arr, orig_arr1, axis=2)

2. hstack操作

与append在axis设置为1时是一样的:

1
2
3
4
orig_arr = np.arange(12).reshape(2,2,3)
orig_arr1 = np.arange(112,124).reshape(2,2,3)
ret_arr = np.hstack((orig_arr,orig_arr1))
ret_arr, ret_arr.shape

3. insert操作

insert操作实际上也是重新生成一个新的数组,它于append的区别是,append只能够再尾部添加,而insert能够指定位置添加,insert也可以指定纬度:

1
2
ins_arr = np.insert(orig_arr, 1, 666)
ins_arr = np.insert(orig_arr, (1,2), 666, axis=2)

4. delete操作

同上delete是新生成数组而不是再原数组上删除,delete可以删除指定位置的元素,多维数组删除一个元素会退化为一维,也可以可以在指定纬度上的数据:

1
2
del_arr = np.delete(orig_arr, 1)
del_arr = np.delete(orig_arr, 1, axis=1)

Mask Array

虽然mask数组在实际工作使用中得很多,但是有些细节还是不是很清楚,所以这里会注重一些使用中没有关注到的细节。

1. 运算优先级

mask数组一般由关系运算得到,对于很多关系运算符需要注意得是其运算优先级,生成的mask与原数组同shape

1
2
nd_arr = np.arange(36).reshape((3,4,3))
ma_ndarr = nd_arr % 5 == 0

2. 作为引索使用

使用mask可以得到子数组,但是多维数组在用mask引索得到得子数组是一维的:

3. 多个mask联合

可以多个mask联合使用,需要使用相应的逻辑运算函数:

1
2
3
4
arr = np.arange(10)
ma_arr1 = arr % 3 == 0
ma_arr2 = arr > 5
comb_ma = np.logical_and(ma_arr1, ma_arr2)

使用set_printoptions函数设置打印格式

在打印计算的结果的时候,最常见的一个问题是结果过小,显示的数字太长,不方便阅读,可以使用set_printoptions的precision参数设置显示精度,除此之外set_printoptions还有更多的设置:

1
2
np.set_printoptions(precision=6)
print(arr1/7)
Compartir

OpenGL纹理映射

纹理的作用

在渲染的时候,为了让图像看起来真实,我们就必须由足够多的定点,指定足够多的定点颜色,但是这样会导致每个模型都会有巨大的开销。为了解决这个问题,我们通常使用2D的图片来代替更多的端点,由于图片上有很多细节,所以在渲染时就可以直接使用图片覆盖在3D物体的表面,让渲染出来的物体看起来更加精致,这就是纹理的作用。

纹理的创建与加载

1. 加载纹理图片

我们知道纹理来源于2D的图片,所以第一步我们需要从原始纹理的图片文件中读出图像的原始数据。有如果使用了opencv的话,可以很方便读取常见格式的图片到mat对象中,但是在一般OpenGL工程中也不会为读取图片特别去配置opencv的环境,一般可以使用一些比较小巧的库。当前比较常用的是std_image.h,它是个单头文件图像加载库,它能够加载大部分流程的图像格式。也正是由于它只有一个头文件,所以将它加载到工程是非常方便的。

std_image.h文件下载地址为:https://github.com/nothings/stb/blob/master/stb_image.h

加载图像文件代码如下:

1
2
3
4
5
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);

由于在load图像的时候申请了内存,所以在使用完成后,需要记得释放内存空间:

1
stbi_image_free(data);

2. 创建纹理

在载获取到了图像的数据后,需要使用OpenGL创建绑定纹理、载入图像数据,除此之外一般会要设置纹理的一些配置属性如:“纹理环绕方式”、“纹理过滤”、“多级渐远纹理”。

  • 纹理环绕方式,可以设置当纹理坐标超过(0, 0)到(1, 1)这个限定范围后的操作,重复这个纹理图像,s、t时其两个方向
  • 纹理过滤,可以指定在超过原始图像分辨率时怎么插值,有放大(Magnify)和缩小(Minify)两种情况

以下为这些操作的基本流程代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
// 为当前绑定的纹理对象设置环绕、过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 加载并生成纹理
int width, height, nrChannels;
unsigned char *data = stbi_load("container.jpg", &width, &height, &nrChannels, 0);
if (data)
{
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB,
GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else
{
std::cout << "Failed to load texture" << std::endl;
}

纹理映射

为了将2D的图片贴附在3D的物体上,有一个非常关键的过程就是要确定纹理图的哪部分对应3D物体的那部分,这就是纹理映射要解决的问题。

1. 纹理坐标

首先要规定的是纹理坐标(Texture Coordinate),由于纹理实际上是一张2D的图像,所以纹理坐标是一个二维坐标,左下角为坐标原点,右上角坐标规定为(1,1),这就是整个纹理坐标系的范围;而另外由于原始纹理图片是有分辨率大小的,所以在映射时需要插值来获取超过了分辨的像素点。

2. 纹理属性

在确定了纹理坐标后,我们只需要将端点坐标与纹理坐标一一对应起来就可以了,使用OpenGL的顶点数组可以规定格式如下:

1
2
3
4
5
6
7
float vertices[] = {
// ---- 位置 ---- ---- 颜色 ---- - 纹理坐标 -
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 右上
0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, // 左下
-0.5f, 0.5f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f // 左上
};

如上代码,在vertices数组中每8个数据代表一个端点,前三个为三维的位置坐标,中间3个为端点的RGB颜色属性,最后两个为其对应纹理贴图中的位置的纹理坐标,在内存中分布如下图:

vertices格式

有了以上端点数组后,就可以指定顶点属性,告诉OpenGL如何解析。前面两个属性不再写了,下面是添加纹理坐标属性的代码:

1
2
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);

3. 在Shader中的处理

一般在顶点Shader中,会将传入的顶点数组,依照设置的顶点属性进行解析,然后传入片元Shader中处理;在片元Shader中需要使用到GLSL供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,在这里我们使用sampler2D;另外我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。

片元Shader的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
#version 330 core
out vec4 FragColor;

in vec3 ourColor;
in vec2 TexCoord;

uniform sampler2D ourTexture;

void main()
{
FragColor = texture(ourTexture, TexCoord);
}

绘制纹理

以上纹理操作全部完成后,就可以绘制了:

1
2
3
glBindTexture(GL_TEXTURE_2D, texture);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

参考

  1. LearnOpenGL CN - 纹理
Compartir

现代OpenGL的基本使用

前言

OpenGL不过作为一个标准不断更新与改进的底层图形库,其新旧版本的使用区别很大,前面我们使用python接口对OpenGL进行简单的使用用的是其老式的方法,现在我们要使用新的特性,因此本文在实际代码实现中结合新版OpenGL的特性进行说明。另外为避免因版本不同而导致的问题,使用其原生的C++的接口通常是一个更好的选择。

在这里我使用环境如下:

  • 系统:Windows 10
  • 编译器:VS 2015

会使用到的概念

1. 标准化设备坐标(Normalized Device Coordinates, NDC)

标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。与通常的屏幕坐标不同,y轴正方向为向上,(0, 0)坐标是这个图像的中心,而不是左上角。最终你希望所有(变换过的)坐标都在这个坐标空间中,否则它们就不可见了。

OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上,一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了。

2. 顶点缓冲对象(Vertex Buffer Objects, VBO)

我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。

顶点缓冲对象是我们在接触现代OpenGL各种对象中最基本的一类。它与其他OpenGL中的对象一样,有一个独一无二的ID,通过glGenBuffers函数生成;同时使用glBindBuffer函数设置缓冲类型;我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中。

3. 着色器

如果我们打算做渲染的话,现代OpenGL需要我们至少设置一个顶点和一个片段着色器。我们需要做的第一件事是用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器,然后编译这个着色器,这样我们就可以在程序中使用它了。

顶点着色器代码如下,功能只是坐标转化,将输入的坐标转化为OpenGL内部能够处理的形式,相当于最简单的着色器了:

1
2
3
4
5
6
7
#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

片段着色器代码如下,功能同样是最简单的了,即设置固定的颜色:

1
2
3
4
5
6
7
#version 330 core
out vec4 FragColor;

void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

4. 编译和链接着色器

上面的着色器代码,我们必须在运行时动态编译它的源码,才能够让OpenGL使用。首先要做的是创建一个着色器对象,注意还是用ID来引用的。首先用glCreateShader创建这个着色器,然后我们把这个着色器源码附加到着色器对象上,然后再编译它。

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

编译两个第三方库

  • GLAD 负责绘图函数的获取
  • GLFW 负责创建绘图上下文和窗口系统管理

1. GLFW库

GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口,所以它非常的小。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入。

为了使用GLFW,同时避免一些兼容性的问题,我们最好自己下载源码编译,GLFW的下载地址为:http://www.glfw.org/download.html,其编译过程在参考链接1中有详细的讲解。编译成成功后会在src/Debug或者src/Release中生成库文件:glfw3.lib

2. 配置GLAD

GLAD是一个开源的库,作用是为了简化OpenGL中的函数调用。在参考链接2中使用的是GLEW库,其作用是一样的。

GLAD使用了一个在线服务,地址为:https://glad.dav1d.de/,选择相应的配置选项,网站就能自动生成库文件。在这里我们只要选择语言为:c/c++、Profile:core、版本选择3.3或者以上就可以了。

GLAD现在应该提供给你了一个zip压缩文件,下载zip文件,包含两个头文件目录,和一个glad.c文件。将两个头文件目录(glad和KHR)复制到你的Include文件夹中(或者增加一个额外的项目指向这些目录),并添加glad.c文件到你的工程中。

新建VS工程

新建vs空项目,将OpenGL、GLFW、GLAD的目录与库文件添加项目属性中,具体说明见参考链接1。对于Windows平台,opengl32.lib已经包含在Microsoft SDK里了,它在Visual Studio安装的时候就默认安装了,但是需要将GLFW、GLAD库加入。

基本窗口代码

下面代码,主要用创建窗口, setVertixData()、deleteObj()、compileShader()、render()函数暂时为空。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
void setVertixData(unsigned int &VBO, unsigned int &VAO);
void deleteObj(unsigned int &VBO, unsigned int &VAO);
void compileShader(int& shaderProgram);
void render(int& shaderProgram, unsigned int &VAO);

int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);

// GLAD init
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}

// 编译shader
int shaderProgram;
compileShader(shaderProgram);

// 设置定点对象
unsigned int VBO, VAO;
setVertixData(VBO, VAO);

// 设置视窗大小
glViewport(0, 0, 800, 600);

// 窗口大小改变时的回调:随窗口改变调整绘制
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

// 在窗口关闭前一直绘制
while (!glfwWindowShouldClose(window))
{
// 接受窗口输入
processInput(window);

// 渲染指令
render(shaderProgram, VAO);

// 检查并调用事件,交换缓冲
glfwSwapBuffers(window);
glfwPollEvents();
}

// 删除创建的对象
deleteObj(VBO, VAO);

glfwTerminate();
return 0;
}

基本绘制代码

以下实现setVertixData()、deleteObj()、compileShader()、render()四个函数,功能是绘制一个三角形,shader代码也直接集成到c++源码中,用字符串数组记录:

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

void setVertixData(unsigned int &VBO, unsigned int &VAO)
{
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};

glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);

// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices,
GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
(void*)0);
glEnableVertexAttribArray(0);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

void deleteObj(unsigned int &VBO, unsigned int &VAO)
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
}

void compileShader(int& shaderProgram)
{
// vertex shader
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

// check for shader compile errors
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

// fragment shader
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

// check for shader compile errors
glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}

// link shaders
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

// check for linking errors
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}

void render(int &shaderProgram, unsigned int &VAO)
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
glUseProgram(shaderProgram);
// seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
}

使用索引缓冲对象改进绘制

绘制复杂的图像,一般需要使用索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。OpenGL调用这些顶点的索引来决定该绘制哪个顶点,如矩形由两个三角形组成,绘制时只要指定四个端点、设置两个三角形分别指定的端点。

对setVertixData()、deleteObj()、render()三个函数做部分修改:

  • 修改函数申明:
1
2
3
void setVertixData(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO);
void deleteObj(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO);
void render(int& shaderProgram, unsigned int &VAO);
  • 函数体修改:
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
void setVertixData(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO)
{
// set up vertex data (and buffer(s)) and configure vertex attributes
// ------------------------------------------------------------------
float vertices[] = {
0.5f, 0.5f, 0.0f, // top right
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
-0.5f, 0.5f, 0.0f // top left
};

unsigned int indices[] = { // note that we start from 0!
0, 1, 3, // first Triangle
1, 2, 3 // second Triangle
};

glGenBuffers(1, &VBO);
glGenBuffers(1, &EBO);
glGenVertexArrays(1, &VAO);

// bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
glBindVertexArray(VAO);

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

// note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
}

void deleteObj(unsigned int &VBO, unsigned int &VAO, unsigned int &EBO)
{
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);

}

void render(int &shaderProgram, unsigned int &VAO)
{
// render
// ------
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// draw our first triangle
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
}

参考

  1. learnOpenGL CN - 创建窗口
  2. OpenGL学习脚印:环境搭建
  3. learnOpenGL CN - 你好,三角形
Compartir

使用python调用OpenGL

前言

OpenGL原生接口是C和C++的,不过OpenGL也提供了python版本,在本文中只会演示OpenGL在python中的最简单使用方法,更具体的功能还需要参考相关库的资料。

另外一部分就是介绍3D加载库Assimp,由于实际的项目都是先用3D软件做好模型,再其他地方使用,所以Assimp是OpenGL项目中相对很实用的库,不过其python版本安装相对复杂,所以这里特殊说明。

PyOpenGL安装

为了用Python使用OpenGL,我们需要安装两个库:

  • PyOpenGL:OpenGL的python接口库
  • PyOpenGL_accelerate:对PyOpenGL进行加速

建议使用conda进行安装,因为conda可以自动安装或更新其他的依赖库如numpy、vs_runtion等等。

1
2
conda install PyOpenGL
conda install conda install PyOpenGL

PyOpenGL官方文档为:http://pyopengl.sourceforge.net/documentation/

演示代码

在PyOpenGL后,可以先用如下简单的代码测试以下,openGL是否能正常工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def drawFunc():
glClear(GL_COLOR_BUFFER_BIT)
glRotatef(0.1, 0.1, 0.5, 0)
glutWireTeapot(0.5)
glFlush()

def main():
glutInit()
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA)
glutInitWindowPosition(0,0)
glutInitWindowSize(400, 400)
glutCreateWindow(b'first')
glutDisplayFunc(drawFunc)
glutMainLoop()

if __name__ == '__main__':
main()

运行以上代码,得到如下窗口:

teapot

关于OpenGL的封装库

在OpenGL中一般的函数命名如下:

<前缀><根函数><参数数目><参数类型>

前缀有gl、glu、aux、glut、wgl、glx、agl等等,分别表示该函数属于OpenGL那个开发库等。所谓开发库,要知道原生的OpenGL是跨平台的,跨平台意味着很多功能是无法统一实现,比如说Windows和X-Window的窗口实现机制是不同的,OpenGL并不关心这些东西,只管画图。所以,基本的OpenGL并没有窗口函数,比如无法创建窗口,无法获得输入等等,这些东西都需要其他的函数库来实现。

在上面的演示代码中我们主要使用两种库,一个是GLU库,它提供了比较基础的命令的封装,可以很简单的实现比较多的复杂功能;而另外一个就是GLUT,glut是不依赖于窗口平台的OpenGL工具包,目的是隐藏不同窗口平台API的复杂度,提供更为复杂的绘制功能。

结合上面的演示代码,可以看到PyOpenGL的用法其实和c++使用原生OpenGL区别非常小。所以更复杂的图形绘制,可以先找c++代码,然后将其变成python代码。

简单Glut的代码框架

虽然现在glut库基本上被其他库代替,但是在旧版的OpenGL中还是使用得比较多的,其基本代码框架python版本如下:

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
36
37
38
39
40
41
42
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

def userinit():
glClearColor( 0.0, 0.0, 0.0, 0.0 )
glColor4f(1.0,1.0,0.0,0.0)

def drawFunc():
glClear(GL_COLOR_BUFFER_BIT)
glBegin(GL_TRIANGLES)
glVertex3f(-0.5,-0.5,0.0)
glVertex3f(0.5,0.0,0.0)
glVertex3f(0.0,0.5,0.0)
glEnd()
glFlush()

# 调整窗口大小回调函数
def reshape(w, h):
glViewport(0,0,w,h);

def keyboardAction(key, x, y):
print(key)
if key == b'q':
exit();

def main():
glutInit()
glutInitDisplayMode(GLUT_SINGLE | GLUT_RGBA)
glutInitWindowPosition(0,0)
glutInitWindowSize(400, 400)
glutCreateWindow(b'first')

userinit()
# glutReshapeFunc(reshape)
glutDisplayFunc(drawFunc)
glutKeyboardFunc(keyboardAction)
glutMainLoop()


if __name__ == '__main__':
main()

3D模型文件与Assimp库

3D软件生成的模型文件有很多格式,如obj、3ds、c4e等,这些文件主要包含如下内容的数据:

  • 定点坐标
  • 法线坐标
  • 纹理坐标
  • 材质、灯光信息

如果希望在OpenGL中使用到这些模型,那么就需要对这些模型文件进行解析,对于某些简单的格式如obj,它是文本格式的文件,每一行的首字符作为标记,可以直接读取解析,但是对于复杂的格式就不那么方便了,最好可以使用他人提供的库来加载。

Assimp则是OpenGl中常用的模型加载库,全称 Open Asset Import Library,它支持很多种格式的模型文件。同样它也提供了python版本pyassimp

pyassimp安装

pyassimpy的安装不一样,我们先使用pip安装后,它仍然是不能导入的,且提示错误:

“ pyassimp.errors.AssimpError: assimp library not found”

这是因为仅仅这样安装只是安装了其python接口,并没有将assimp库文件安装。通常我们需要下载其源码自己编译,对于windows系统就是编译生成其对应的.dll文件。下载地址:http://assimp.sourceforge.net/main_downloads.html

由于assimp使用cmake管理的,所以在编译assmip前,先确保cmake安装。关于cmake在windows下如何于VS协同使用可以参考如下链接:CMake入门1——CMake与VS编译器和nmake的结合使用

在使用cmake配置项目的时候,需要注意python的是64bit还是32bit,如我的是64bit,在选择编译器就要选择对应的VS64bit版本,否则python将仍然无法导入assimp:

cmake

如果cmake没有选择正确,而在VS中又强制编译另一个版本的库,则编译时会如下如下错误:

“>x64\Debug\adler32.obj : fatal error LNK1112: 模块计算机类型“x64”与目标计算机类型“X86”冲突”

在编译完成后将生成的.dll库文件如下:

cmake

我们需要将其放入到python能够找到的目录下,我是直接放到site-packages/pyassimp的目录下。

完成上面操作后,可以测试运行如下,检查是否成功导入:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pyassimp

objpath = r'1.obj'
scene = pyassimp.load(objpath)

assert len(scene.meshes)
mesh = scene.meshes[0]
print(mesh)

assert len(mesh.vertices)
print(mesh.vertices[0])

pyassimp.release(scene)

assimp读取模型数据结构

当导入一个模型文件时,即Assimp加载一整个包含所有模型和场景数据的模型文件到一个scene对象时,Assimp会为这个模型文件中的所有场景节点、模型节点都生成一个具有对应关系的数据结构,且将这些场景中的各种元素与模型数据对应起来。下图展示了一个简化,的Assimp生成的模型文件数据结构:

assmip

下载pyassimp源码安装包中,在其中的scripts文件中,提供了使用例子,可以参考其中的代码学习。源码安装地址:https://pypi.org/project/pyassimp/4.1.3/

参考

  1. 用PyOpenGL叩开3D的心扉——OpenGL全解析
  2. OpenGL学习脚印:模型加载初步-加载obj模型(load obj model)
  3. Opengl学习之模型加载——Assimp
Compartir

OpenGL初步认识

前言

关于OpenGL的一些基本认识,以下为两个很好的现代OpenGL教程,其链接以及GitHub代码地址如下:

OpenGL的理解

OpenGL(Open Graphics Library)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口。为了跨平台,实际上狭义上的OpenGL只是一个标准,各种硬件公司遵循其标准来生产硬件,在软件层面上按照该标准也有多种代码实现,所以才会类似GLEW、GLUT、GLFW这些库的存在。

OpenGL是一种客户端-服务器(client-server)类型的系统。我们编写的程序就是一个客户端,而我们的计算机图形硬件制造商提供的OpenGL的实现就是服务器。在一些OpenGL的实现里(例如一些和XWindow System相关的应用),客户端和服务器可能会在不同的机器上运行,中间用网络连接。

状态机

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。假设当我们想告诉OpenGL去画线段而不是三角形的时候,我们通过改变一些上下文变量来改变OpenGL状态,从而告诉OpenGL如何去绘图。一旦我们改变了OpenGL的状态为绘制线段,下一个绘制命令就会画出线段而不是三角形。

基本图形绘制方式

1. 传统方式绘制

传统绘制方式在OpenGL新版本中已经废弃,虽然在兼容模式下还能工作,但不建议使用。传统绘制方式的包括以下两方面的内容:

  • 立即模式(Immediate Mode):使用glBegin…glEnd方式制定绘制方式,在这两个函数对之间给出绘制的数据,固定渲染管线,这种方式称为立即模式
  • 显示列表(Display List): 显示列表是一组存储在一起的OpenGL函数,可以再以后执行。调用一个显示列表时,它所存储的函数就会按照顺序执行。显示列表通过存储OpenGL函数,可以提高性能。如果需要多次重复绘制同一个几何图形,或者如果有一些需要多次调用的用于更改状态的函数,就可以把他们存储在显示列表中。

2. 现代绘制方式

现代的绘制方式主要有三种方法:

  • 顶点数组绘图:使用顶点数组方式,需要利用glEnableClientState开启一些特性,这里开启顶点数组特性使用glEnableClientState(GL_VERTEX_ARRAY)。使用顶点数组时,用户定义好存储顶点的数据,在调用glDrawArrays、glDrawElements之类的函数时,通过glVertexPointer设定的指针,传送数据到GPU。当调用完glDrawArrays后,GPU中已经有了绘图所需数据,用户可以释放数据空间。
  • VBO和VAO绘图:VAO即Vertex Array Object,是一个包含一个或多个VBO的对象,被设计用来存储一个完整被渲染对象所需的信息。VBO即Vertex Buffer Object,是一个在高速视频卡中的内存缓冲,用来保存顶点数据,也可用于包含诸如归一化向量、纹理和索引等数据。
  • 结合Shader绘图

在旧版本的OpenGL中,是通过glVertex,glTexCoord和glNormal函数把每帧数据发送给GPU的。在现代OpenGL中,所有数据必须通过VBO在渲染之前发送给显卡。当你需要渲染某些数据时,通过设置VAO来描述该获取哪些VBO数据推送给shader变量。

3. 各种中绘图方式利弊

  1. 使用立即模式,缺点很明显,数据量大一点的话,代码量增加,而且数据发送到服务端需要开销
  2. 使用显示列表,显示列表是一个服务端函数,因此它免除了传送数据的额外开销。但是,显示列表一旦编译后,其中的数据无法修改
  3. 使用顶点数组,可以减少函数调用和共享顶点数据的冗余。但是,使用顶点数组时,顶点数组相关函数是在客户端,因此数组中数据在每次被解引用时必须重新发送到服务端,额外开销不可忽视
  4. 使用VBO在服务端创建缓存对象,并且提供了访问函数来解引用数组,如:例如在顶点数组中使用的函数如glVertexPointer(),glNormalPointer(), glTexCoordPointer();另外,不像显示列表,VBO中数据可以通过映射到客户端内存空间而被用户读取和更新;VBO的另外一个优势是它像显示列表和纹理一样,能和多个客户端共享缓存对象。可见使用VBO优势很明显

OpenGL pipeline

3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分的作用:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。

具体到不同版本的OpenGL中pipeline的过程会有不同,现代OpenGL程序中允许用户自己定制着色器,这使得绘图更灵活,下面是一个比较典型的示意图:

其中:顶点着色器和片元着色器是OpenGL整个渲染管线必须要有的过程,其他的着色器可以根据需要进行选择,图元是基本的绘制形状,如点、线、三角形;另外除了第一阶段准备定点数据是在CPU中做,其他的一般都是在GPU中工作的。

opengl pipeline

其中顶点着色器(Vertex Shader)比较重要,通常会有如下作用:

  • 顶点变换 根据模型视图和投影矩阵变换
  • 光照计算
  • 纹理坐标变换(纹理矩阵)
  • 材质状态、纹理坐标生成

Shaders

Shaders在现代OpenGL中是个很重要的概念。Shaders是一段GLSL小程序,运行在GPU上而非CPU。它们使用OpenGL Shading Language (GLSL)语言编写,看上去像C或C++,但却是另外一种不同的语言。使用shader就像你写个普通程序一样:写代码,编译,最后链接在一起才生成最终的程序。在旧版本的OpenGL中,shaders是可选的,在现代OpenGL中,为了能在屏幕上显示出物体,shaders是必须的。

Shader的中文是着色器,但是它的作用不仅仅只做着色,shaders实际上干了啥,这取决于是哪种shader。

纹理

要使渲染的物体更加逼真,一方面我们可以使用更多的三角形来建模,通过复杂的模型来逼近物体,但是这种方法会增加绘制流水线的负荷,而且很多情况下不是很方便的。使用纹理,将物体表面的细节映射到建模好的物体表面,这样不仅能使渲染的模型表面细节更丰富,而且比较方便高效。纹理映射就是这样一种方法,在程序中通过为物体指定纹理坐标,通过纹理坐标获取纹理对象中的纹理,最终显示在屏幕区域上,已达到更加逼真的效果。

光照

要模拟现实的光照是困难的,例如实际光照中,一束光可以经过场景中若干物体反射后,照射到目标物体上,也可以是直接照射到目标物体上。其中经过其他物体反射后再次照射到目标物体上,这是一个递归的过程,将会无比复杂。因此实际模拟光照过程中,总是采用近似模型去接近现实光照。Phong Reflection Model是经典的光照模型,它计算光照包括三个部分:环境光+漫反射光+镜面光。

  • 环境光:环境光是场景中光源给定或者全局给定的一个光照常量,它一般很小,主要是为了模拟即使场景中没有光照时,也不是全部黑屏的效果
  • 漫反射光:漫反射光成分,是光照中的一个主要成分,漫反射光强度与光线入射方向和物体表面的法向量之间的夹角相关
  • 镜面反射光:镜面光成分模拟的是物体表面光滑时反射的高亮的光,镜面光反映的通常是光的颜色,而不是物体的颜色,计算镜面光成分时,要考虑光源和顶点位置之间向量L、法向量N、反射方向R、观察者和顶点位置之间的向量V之间的关系

我们实现的光照计算是在片元着色器中进行的,这种是基于片元计算的,称之为Phong
shading,在过去OpenGL编程中实现的是在顶点着色器中进行光照计算,这是基元顶点的计算的,称之为Gouraud Shading。

参考

  1. OpenGL学习脚印:基本图形绘制方式比较
    2.【OpenGL】理解一些基本问题
  2. OpenGL渲染管线(rendering pipeline)
  3. Rendering Pipeline Overview
  4. OpenGL学习脚印:绘制一个三角形
  5. OpenGL学习脚印: 光照基础(basic lighting)
  6. 现代OpenGL教程 01 - 入门指南
  7. learnOpenGL CN - OpenGL
  8. learnOpenGL CN - 你好,三角形
Compartir

GMM高斯混合模型的使用

前言

在了解过EM算法后,再来看GMM就轻松多了,当然对于我来说除了GMM的基本原理外,更重要的是需要利用GMM算法来做视频的运动检测。

GMM简介

GMM是Gaussian Mixed Model的简称,中文名为高斯混合模型,基本原理其实十分简单,就是用多个不同的高斯模型线性叠加来拟合实际的数据分布。如下图为截取网友介绍GMM博文的图像,其中使用了两个高斯分布来拟合的实际的数据点分布:

GMM

假定有随机变量x,混合高斯模型的公式表达形式为:

GMM公式

p(x)表示x发生的概率,它由K个不同的高斯分布组成,πk相当于每个分量的权重,详细说明可以看参考中第一篇博文的说明。

GMM和EM算法

虽然GMM的基本原理很简单,但是使用GMM要得到估计的最佳结果却不容易,对上图来说就就是两个高斯的均值、方差以及各自占的权重。为了计算得到GMM的最优的估计参数就需要使用到EM算法,所以理解GMM方法的难点实际上是需要理解EM算法在GMM中的使用,而实际上EM算法真是不是那么好理解。

对于EM算法我在上篇单独做了简要的说明,我们可以看到GMM这样的形式非常适合用EM算法来就解,因为EM算法来说就是为了解决由于隐变量的存在导致目标参数相互影响的问题的方法,在GMM中这个隐变量就是每个高斯分量的类型;另外GMM的每个分量的形式都是已知的,都是高斯分布,所以使用EM算法计算最大似然也是非常方便。

使用GMM做检测视频的运动检测

在视频处理中GMM是一种非常常用且有效的运动检测方法,它的主要作用是将场景中的前景和背景分离。它是对图像的每个像素建立多个高斯模型,每个模型包含均值、方差、权值和匹配数4个参数,我们可以看到,除了匹配数其他参数都是上面公式里包含了的。在GMM使用前需要设置好高斯模型的个数,这是需要人为指定的。然后根据第一帧图像初始化第一个高斯模型的参数,而其他的高斯模型则使用0初始化。

在后续帧输入时,使用当前图像的每个像素值对于其对应的高斯模型进行匹配,一种匹配的方法是:计算当前像素值与要匹配的高斯模型的偏离均值的大小,若小于阈值则匹配成功。完成匹配的操作后,使用新匹配的数据更新模型的参数,最后进行高斯模型排序,不过这个排序是根据权值方差来定的,具体可以看参考链接2。

图像的背景为每个像素的灰度值其对应的第一个高斯模型的平均值,当当前图像像素值与已存在的第k个高斯模型匹配,并且前k-1个高斯模型的权值之和小于阈值bg_threshold ,则将当前像素归为背景,像素值设为0,否则归为前景,像素值设为255。

在opencv中的使用

GMM在opencv中有对应的算法实现:BackgroundSubtractorMOG和BackgroundSubtractorMOG2,BackgroundSubtractorMOG2是在BackgroundSubtractorMOG的改进,具体可以查看opencv的文档。

opencv的官方文档中提供了简单的使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import numpy as np
import cv2 as cv

cap = cv.VideoCapture('vtest.avi')

fgbg = cv.createBackgroundSubtractorMOG2()

while(1):
ret, frame = cap.read()

fgmask = fgbg.apply(frame)

cv.imshow('frame',fgmask)
k = cv.waitKey(30) & 0xff
if k == 27:
break

cap.release()
cv.destroyAllWindows()

参考

  1. 高斯混合模型(GMM)及其EM算法的理解
  2. OpenCV之基于GMM的运动目标检测
Compartir

EM算法理解

前言

这几天开始研究一些视频处理的算法,在学习GMM(高斯混合模型)时硬是弄不懂其优化原理,什么E步、什么M步完全搞不懂,到最后才发现其实GMM是使用了EM算法进行优化求解,但是大多数介绍GMM的文章是不会仔细说明EM算法的,所以我在不了EM算法的情况下能懂才怪了,更何况EM算法本身就一个难理解的东西。

两篇介绍EM算法优秀博文

EM算法是Expectation Maximization Algorithm的简称,中文可以翻译为期望最大化算法,它是一种基于统计估计的迭代算法,它能够用在一些无监督的方法上。关于EM算法我在网上找到了两篇非常好的文章:

  1. EM算法(Expectation Maximization Algorithm)
  2. 如何感性地理解EM算法?

第一篇博客,作者从实例出发,一步步非常详细的推理、论证和说明EM算法的基本原理,甚至连需要的数学背景知识都有简要的介绍。这篇文章最让我敬佩得就是,作者认真细致得写作态度,各种数学公式、图片、文本格式都是是否的整洁和漂亮,而从内容上来看,这也许是EM算法总结得最完善、最严谨的中文学习博客了。但是说实话,在看完这篇文章后,我仍然时一头雾水,因为数学公式太多了看得晕,直到我看完第二篇博客。其实我们从标题也可以看出,第二篇博客作者的目的就是希望读者能够更加直观的理解EM算法,所以在第二篇在这篇博文中,基本上没有数学公式,只有最基本的概率计算。同时作者使用最简单的例子由浅入深,从不同的层次带领读者直观的体验EM算法处理问题的思路和方法,所以阅读时建议先看第二篇再看第一篇。

这两篇文章分别从两个不同的角度将EM算法说明得非常得详尽了,在我看来因该称得上是初学者学习EM算法的最佳资料了。甚至在看过这两篇文章,我觉得已经没有再次总计的必要了,因为该说的东西这两篇文章都说了。不过我还是想自己记录一下,一方面是基于这两篇文章中的难点内容做一些进一步的说明,另一方面则是记录一些我自己的理解。

关于隐变量的理解

EM算法中比较关键的一点就是要理解隐变量是什么东西。以上面两篇博文里面都提到的硬币为例,在两硬币的例子里每组投掷同一个硬币多次,我们能观测到了每组硬币的投掷的结果,但是却不知道每组投掷的是哪一个硬币,这里每次投掷的硬币类型就是隐变量;而在三硬币模型里,每组投两个硬币1次,第一硬币是已知的,第二个硬币要根据第一个硬币的结果来确定,所以第二个硬币的类型(或者说是第一个硬币的投掷结果)就隐变量。

从以上我们可以看到,一般我们能通过观测事件发生的结果,来预测事件发生的概率,但是如果观察的结果是受到令一个事件的影响的话,我们可能就没法直接估计了。所以我们需要先估计隐变量,然后再估计我们需要的结果,这就是EM算法中需要引入隐变量的意义。

EM算法流程

在前面的博文中有提到EM算法分两个步骤:E步和M步,不过再开始之前,我们需要对最终的结果分布假定一个初值,在初值的基础上进行E步:利用初值计算估计每组观察隐变量的分布,然后M步:对E步得到的隐变量分布和原始观察结果,计算其log似然函数的最大值,计算结果更新初值,重复迭代。

在M步求解最大值的时候,可以求参数的的偏导数为0,得到结果,如果不方便计算也可以用梯度下降得到近似解。

Compartir

全卷积神经网络FCN介绍

前言

FCN是Fully Convolutional Networks的缩写,中文即为全卷积神经网络,虽然从字面上看FCN只是在CNN的基础上把原来不是卷积的网络层换成卷积层,但是FCN出现的作用远远不是从这字面上的意思能够传达的。另一方面,当我们在查看FCN资料的时候,总是会看到大家都将它与semantic segmentation(语义分割)绑在一起说明,这显然这不是偶然,这是因为FCN本来就是为更好的使用神经网络解决semantic segmentation问题,在CVPR 2015拿到best paper候选的论文:Fully Convolutional Networks for Semantic Segmentation中首次提出的。

那么什么是semantic segmentation语义分割呢?在我的理解中语义分割是更高层次的图像分割,相比图像分割只是简单的将图像分成不同区块碎片,语义分割要求分割的物体是由意义的整体。如下图,语义分割能够在整张图像中识别分离出自行车和人,而图像分割则不能达到这一目标,只能分割出意义不明的区域。

semantic segmentation

CNN和语义分割

正如我们提到机器学习就想到回归和分类一样(当然深度学习更加注重分类问题),当我们提到CNN就肯定会将它与图像分类问题联系在一起。确实CNN在处理图像分类问题上是非常强大的,但是毕竟图像分类问题太单一了,在复杂的视觉应用场景中,仅仅通过图像分类是远远达不到最终的目标的,那么我们是否能够用CNN来解决其他方面的图像处理的任务呢,如语义分割?

答案当然是可以的,基于CNN的分割的做法是:对每个像素点,都使用其领域块作为图像导入到CNN中做一次计算,得到预测分类的结果,把相同分类的像素点分离出来就完成了分割。这样做的缺点是:1. 存储开销大;2. 计算效率低,相邻像素的是分开预测导致很多计算是重复的;3. 像素领域块的大小限制了感知区。

从另外一个角度去分析,CNN在图像分类任务中做得很好,主要原因是CNN的多层结构能够让其能够从浅层特征中提取得到更加抽象的特征,这些高级的特征可以降低空间位置、大小、角度的因素的影响,这对于识别和分类帮助很大。但是高度的抽象也让它让丢失了对物体细节的描叙,另外经典的CNN在卷积层之后使用全连接层得到固定长度的特征向量进行分类,全连接层进一步的丢失了图像的空间信息。正是如此,CNN要做到像素级的分割任务就很有难度了。

FCN对CNN的改进

1. 将全连接层改为卷积层

FCN在CNN的基础上对明显的改变就是,将原来CNN接近输出端的全连接层都改为卷积层。

在神经网络计算时,全连接层和卷积层之间不同点就是,卷积层中的神经元只与输入数据中的一个局部区域连接,并且在同一层上卷积核的参数对这层上的所有神经元是共享的。由于其计算形式的都是点积,所以两者实际上是可以转化的。我们可以用nxnxN的卷积核来代替一个前面是nxnxK的tensor链接上有N个神经元的全连接层,而两者是等价的。

将全连接层换成卷积层后有什么好处呢?

  1. 首先就是不再限制输入图像尺寸的大小,因为原来是全连接的神经元数量是固定的,输入尺寸不对计算出错;但是换成卷积后,由于卷积层是共享权值的,所以即使输入尺寸不对,卷积运算依旧可以进行。
  2. 其次就是可以在同一次网络的前向计算中得到不同区域的结果,这正是由于FCN不再限定输入图像尺寸的结果,所以当我们用大尺寸的图像做输入时,得到的结果不是一个分类或特征结果,而是一个分类图或者特征图(在FCN一般叫heatmap),这个图分别代表着输入图中对应的各个区域经过网络计算预测的结果。这样比起必须将原始大图分成不同区域输入到网络中重复计算才能得到各部分结果的CNN的要高效多了。

下图为FCN和CNN的对比结果:

FCN VS CNN

2. 将上采样的加入的网络中

当我们换掉的全连接层后,就解除了很多限制,前面我们就讲了我们将不再限定输入图像的尺寸。那么进一步我们希望输出的也不仅仅是一个数值或一组数值结果,而是一张图像,这对于像语义分割在内的像素级的图像处理任务是非常有利的。

以语义分割为例,我一般会希望得到一张与原图像同样尺寸的已经分割好的图像,那么应该怎么得到这样的结果呢?我们知道图像经过的卷积和池化后,尺寸会越来越小、特征会越来越多,最后一层卷积层输出的结果在FCN中被称为heatmap热图。为了输出原始图像的尺寸,我们需要在heatmap后面加入上采样层,将heatmap放大到原图尺寸,并且利用heatmap中高维的特诊信息确定放大后像素的分类,这就是一个end-to-end(端到端)的系统。

关于上采样具体是怎么实现的,我自己也还没有理解得很清楚,但是原理上可以肯定确定的,上采样一般使用的是双线性插值,然后通过卷积进行预测分类来实现的。

3. skip layers

直接从最后的卷积层的结果上采样会得到图像时比较粗糙的,为了得到更加精细的结果,一般采用skip layer的方法,对浅层的卷积输出的结果进行减小步长的upsampling,得到的fine layer和高层得到的coarse layer做融合。如下图所示:

skip layer

参考

  1. 论文阅读笔记:Fully Convolutional Networks for Semantic Segmentation
  2. 全卷积网络(FCN)与图像分割
  3. 全卷积网络 FCN 详解
  4. 谈一谈深度学习之semantic Segmentation
Compartir

对极几何的理解

前言

关于对极几何,从去年最开始接触视觉SLAM的时候就经常看到,那时候还以为它是一门几何学科,觉得要了解一门新的学科范围太大,于是就鸽了没有去看。最近开始研究立体视觉,这次是躲是不过了,开始学习吧。查过一通资料才恍然大悟,原来这东西哪是是什么新学科,只是一种特定情况下的几何模型而已,所以这些专有名词就是喜欢吓唬人。

不过话虽然是这样说,但是我丝毫也没有看轻它的意思,在多角度视觉成像的分析中,对极几何其研究基础。

对极几何是什么?

在我查找的资料中,我觉得比较好的解释是:对极几何是相机在两个不同的位置生成的两幅图像,其拍摄位置和生成图像之间的特殊几何关系。其基本几何模型如下图所示:

基本几何

对极几何有什么用?

在介绍对几何详细概念前,我想先说一下它有什么用,这是我在写这这篇文章的时候一直在想的问题,从单纯的概念解释并不是那么直接就理解到它的应用场景,那对极几何有什么用呢?

其一就是立体匹配问题,对于已知两视角空间位置关系的情况下,由于对极几何这个几何模型限定的约束条件,使得在立体图像对上搜索空间上的分别在两个图像中的位置只需要相应的对极线上找,把原来的二维搜搜问题,直接简化为一维搜索,双目测距就是这方面得应用之一。

其二就是确定两个拍摄点相对位置与姿态问题,在未知视角位置的情况下,通过搜索图像对中的匹配点,可以求得两个位置和姿态得相对关系,这一点常用在机器人导航、地图得生成、三维重建等方面。

基本概念

  • 极点(Epipoles):两个相机得基线与两个成像平面得交点,如上图中的e0、e1
  • 极线(Epipolar Lines):空间中点在成像平面上的投影点与极点的连线,如上图中的l0、l1
  • 极平面(Epipolar Plane):空间中的点与两个相机的光轴中心点所组成的平面,如上图c0、c1、p所在的面

对极约束、基础矩阵和本质矩阵

对于对极几何,通过上图的几何模型,我们能得到不少有用的结论,但是去除那些细枝末节的东西,其实只有对极约束这个性质才是描叙对极几何最本质特点的性质。

对极约束

如下图所示,假定相机参数已知,那么对于空间中P点,它将和两个相机的中心点O1和O2,唯一确定对极几何的几何关系,极点、极线、极平面都将确定,而对极约束描叙的就是:在平面1上成像为p的所有空间点,其必定投影在平面2的极线上,反之亦然。

epipolar_constraint

在实际应用中,我们可以直接使用这条性质,但是却不是那么的直接,所以在数学上我们引入的基础矩阵和本质矩阵,用一个非常简洁的等式就概括这样一个关系。我发现有一个很有意思的现象:往往数学的描叙是不直接、更难理解的,但是在这里我反倒觉得使用了基础矩阵或本质矩阵的等式是更直接、更清晰的。

基础矩阵和本质矩阵

为了表示对极约束中在两个成像平面上的点的相对关系,在数学上我们只需要加入一个矩阵(本质矩阵或者基础矩阵)就可以简洁的写出两者的等式关系。

对于本质矩阵E其矩阵等式为:

$pl^T E pr = 0$

对于基础矩阵F其矩阵等式为:

$cl^T F cr = 0$

其中pl和cl表示空间中的点投影在左视图的位置,pr和cr为其投影在右视图中的位置,p和c表示视图中的点在不同坐标下的表示。根据上面的任意一个等式,当我们已知空间中一点在左右视图中的两个位置坐标和基础矩阵(或本质矩阵)这三个中的两者,我们就能够计算得到第三个未知量(对于求E和F来说,可能需要多个点联立求解,因为这两个矩阵中的未知量比较多)。

本质矩阵(Essential Matrix)

上面两式,我们可以看到其等式形式是一模一样的,唯一的区别就在于,两种是在不同坐标系中的表达。本质矩阵E连接的是不同视角下摄像机坐标系下两个投影点的关系,所以pl和pr是在摄像机坐标系下的坐标。

假定两个投影平面的坐标系转化为:旋转R和平移T,那么本质矩阵E与R和T由如下关系(公式推导就省略了):

$ E = [T_x] \cdot R$

其中(向量叉乘转矩阵的方法):

$ [T_x] \cdot R = T \times R$

基础矩阵(Fundamental Matrix)

基础矩阵F链接的是在图像坐标系下两个视图图像中的坐标像素坐标的关系,所以cl和cr是图像的像素坐标。我们知道从摄像机坐标系到图像的像素坐标系,是由摄像机的内参矩阵K来确定的,因此基础矩阵与旋转矩阵R和平移向量T之间的关系还需要有摄像机的内参矩阵加入:

$ F = Kl^{-T} \cdot [T_x] \cdot R \cdot Kr^{-1}$

显然其与本质矩阵的关系如下:

$ F = Kl^{-T} \cdot E \cdot Kr^{-1}$

参考

1.对极几何(EpipolarGeometry)

  1. 二视图从运动到结构—-对极几何和基础矩阵
Compartir

Spatial Transformer Network了解

前言

最近在看深度神经网络论文的时候,遇到了STN这个名词完全不知道是什么东西,后面搜索后才知道这个原来是Spatial Transformer Network的缩写,一种能做空间变换的神经网络结构。这可让我长见识了,在我的认知中一直以为卷积神经网路就只是一层一层的做图像的特征提取,没想到空间仿射变换也能用神经网络去做。我觉得详细了解其实现细节对于我理解论文后面的内容是很有必要的,所以将学习的一些知识点记录在这篇文章中。

简介

Spatial Transformer Network是Google旗下的DeepMind的四位剑桥Phd研究员在2015年他们的论文中提出来的,他们针对CNN的特点,构建了一个新的局部网络层,称为空间变换层,它能将输入图像做任意空间变换。

我们知道在传统图像处理中空间变换是很重要的,因为图像本来就是空间中的物体在平面上的投影,而在不同视角得到的图像是不一样的,通过空间变换我们能够建立其相同场景在不同视角的位置和姿态的关系,在识别上我们希望我们的识别算法能够在各种不同的视角的图像识别出同样的物体。STN则为我们提供一种通过构建简单神经网络的方法来实现空间变换的方法,这样我们能够利用现有强大的神经网络训练方法来解决和优化各种空间变换问题,这是我个人觉得STN非常有价值的地方。

另外,STN 能够在没有标注关键点的情况下,根据任务自己学习图片或特征的空间变换参数,将输入图片或者学习的特征在空间上进行对齐,从而减少物体由于空间中的旋转、平移、尺度、扭曲等几何变换对分类、定位等任务的影响。加入到已有的CNN或者FCN网络,能够提升网络的学习能力。

STN原理

STN依赖如下三种传统图像处理技术:

  • 仿射矩阵
  • 逆坐标映射
  • 双线性插值

放射变换矩阵

对平面图像放射变换矩阵就是一个2x3的齐次形式的矩阵,以此以实现平移、旋转、缩放、剪裁,这个传统空间变换的基本概念,这里就不细写了。

逆向坐标映射

这仍然是图像处理中常用的手段,当我已知一个空间变换的映射后,要得到目标图像,正向的方法是:对源图像做正映射从而得到目标。正映射的缺点是目标图像受限于原图,如当变换是带有放大操作时,目标图像必然会有某些像素点没有值,因为这些点的正变换在源图像中是没有点与之对应。所以更好的办法时做逆向坐标映射,逆向映射是正映射的反映射,使用逆向映射目标图像的任意点总能找到源图像中的位置或者坐标,当然这个坐标有可能是小数或者超出原图像的范围。

对于仿射矩阵,逆映射是正映射放射矩阵的逆矩阵。

双线性插值

承接上面逆向坐标变换图像放大操作的例子,在正向映射中为目标图像为空洞的那些像素点,在逆向映射中能够找到原图像中与之相对的位置的坐标,只是这些坐标带有小数,我们需要使用插值的方法对这些像素点进行补值,图像中插值方法最常用的就是双线性插值了。

双线性插值相对来说是一种比较简单但是有能保证一定质量的插值方法,公式这里就不写了。

STN网络结构

STN主要由三部分组成:

  • localisation network
  • grid generator
  • sampler

下图为STN基本结构示意图

Spatial Transformer Network

Localisation Network

Localisation Network可以翻译成定位网络,它的输入是feature map,输出就是仿射变换矩阵。在上面的示意图中,我们看到在Localisation Network画了两对象体,前面的作用是做计算,可以是全连接也可以是卷积,后面是回归层作用是将映射做归一化。

grid generator

grid generator的作用就是生成双线性插值的坐标网络,它实现的就是逆向坐标映射的结果。

sampler

sampler就是使用双线性插值的得到目标图的过程,作者也叫这一步Differentiable Image
Sampling,是希望通过写成一种形式上可微的图像采样方法,目的是为了让整个网络保持可以端到端反向传播BP训练。

总结

以上,具体结构还是的看原始论文,关于STN的初步内容就介绍完成,写的非常简略的原因是,我参考的大部分文章都对STN网络结构了解到非常深入的程度,所以我也怕写错,详细的东西如还是需要仔细看论文。

参考

  1. Spatial Transformer Networks(空间变换神经网络)
  2. 【论文笔记】Spatial Transformer Networks
Compartir