nnedi3 相关代码学习
引言
其实不知道有没有必要像这样,为并不是很复杂的源码煞有介事地写一份包含个人感想的阅读笔记。但不得不说,nnedi3 相关的内容,从我最开始接触图像处理,就一直萦绕在心头。一直觉得 nnedi3 很神奇,抗锯齿用它,重采样放大也用它,因为用了神经网络更显得高大上。另一方面,随着 Deep Learning 浪潮涌起,有些人开始质疑对 Deep Learning 的跟风。于是有种错觉,用传统神经网络的 nnedi3,既套上了光环,又躲过了质疑。
起初接触 nnedi3 是通过 nnedi3_resize16 这个流传甚广的缩放脚本。之后在我学会 deband 与边缘检测后,很长时间没有继续学新东西,几乎过了一年,我才接触到 nnedi3 的抗锯齿用途。又跨过大半年,在我熟悉 Github 之后,到第二年的秋天,我又看到了优化版的 znedi3 和 nnedi3cl。某种角度上,nnedi3 贯穿了我到目前为止的图像处理学习过程。
我已经忘记之前是在哪里看到,说 nnedi3 没有开源。我也模糊得记得,找过 eedi 系列滤镜,没找到代码(事实上也是开源的)。于是交错的印象下,在我开始写 C++ 之前,没有再深入了解 nnedi3。即使是之后开始写 VapourSynth Plugin,也是模仿着 Deblock 和 nnedi2 的框架(话说这对初学者而言,和 nnedi3 比也没什么区别喂喂,只是我当时选择性地无视了)。
BTW,学习代码不是自己写代码,有时候需要不求甚解,明白意思即可,不然也只是浪费时间。
代码概览
代码框架
VapourSynth-NNEDI3CL 是 VapourSynth 框架下进行了 OpenCL 优化的 nnedi3 实现,除了 OpenCL 配置文件外,只有一份 nnedi3cl.cpp,代码结构也不复杂,VapourSynth Plugin 框架的四个函数 + 一个 process() 性质的函数。
重新审视一下 VapourSynth Plugin 框架的四个函数,初始化函数 Init() 和 释放内存函数 Free() 不必多说,从计算内容的角度看,GetFrame() 函数是与图像内容相关的计算,会用到像素点的数据,而 Create() 函数(除获取参数外)则是无关像素点数值的计算,至多会用到视频格式的信息。从两个函数的名字上看也能看出这一点。
NNEDI3CL 因为涉及 OpenCL,且参数较多,Create() 函数中报错信息占了较长的篇幅。nnedi3 最原始的用途是隔行插值反交错,所以在 GetFrame() 函数中,比只处理逐行的滤镜,多了 field / 场信息的判断。
大致浏览完这些信息,核心处理从nnedi3_weights.bin
文件入手。
主要数据
1 |
|
其中queue
、kernel
用于 OpenCL 框架,三个image2D
类型的变量src
、dst
、tmp
用于储存图像,余下三个变量则是与神经网络权值相关。
在 Create() 函数内,weights0
、weights1
都是float
指针。
cl_mem
类型用于在设备上分配内存,cl_mem
类型是”Memory Object“的句柄,提供了一种内存抽象。
神经网络
IO 部分
由于使用传统神经网络,需要读入权值文件nnedi3_weights.bin
。IO 部分基于头文件<cstdio>
。
除了常见的std::fopen
、std::fclose
外,还用到了下面这些函数。
std::fread
-std::size_t fread(void* buffer, std::size_t size, std::size_t count, std::FILE* stream)
:从输入流读取至多 count
个对象到数组 buffer
中,返回成功读取的对象数。
std::rewind
-void rewind(std::FILE* stream)
:移动文件位置指示器到给定文件流的起始。
std::ftell
-long ftell(std::FILE* stream)
:返回文件流文件位置指示器的当前值。
读入的权值数据保存在bdata
中。
1 |
|
权值计算的后续
Create() 部分的权值有点复杂,先看看 Create() 权值计算结果是怎么用到 GetFrame() 上的吧。
如后文的”OpenCL 变量“部分所说,三个权值变量被 GetFrame() 调用的只有两个,weights0
和weights1
,被调用的位置和次数一样,只是weights1
在 Free() 函数中多了一步释放。
1 |
|
两个变量在三个分支的if-else
中同时出现了四次,伪代码如下。
1 |
|
很明显,是从 x,y 两个方向分别处理。在这四次调用中,weights0
和weights1
发挥的作用是等价。
进而来整体看下 GetFrame() 函数及 process() 函数(在 NNEDI3CL 里写作 filter()
),常规而且仅有骨架,上面的伪代码便是核心而唯一的操作,除此之前唯一陌生的地方就是增加了 field 信息的判断。
于是,可以转向 .cl 文件看核心计算了。
OpenCL 核心计算
这部分是 .cl 文件中的代码学习笔记。
命名空间(好像说法不太对..)kernel
下的函数有两个重载,分别对应float
类型和 uint 类型,看看其中一个。(删去了"\n"
)
1 |
|
和前面filter()
函数中的核心处理相对应。
追踪一下weights0
的传递过程。
1 |
|
(这个PRESCREEN()
是啥…)
OpenCL 接口部分
这部分是 Create() 函数中出现的 OpenCL 相关内容。
相关函数
clCreateImage
1 |
|
Creates a 1D image, 1D image buffer, 1D image array, 2D image, 2D image array or 3D image object.
创建一维图像、一维图像 buffer、一维图像数组、二维图像、二维图像数组或者三维图像对象。
OpenCL 变量
d->weights1
结构体内的weights1
定义如下。
1 |
|
其中,mem
的主要数据来自desc
。
desc
的数据,除了一些格式上的常数,一是来自dims1
,一是来自权值weights1Buffer
,也就是三个结构体内的数据之一。
dims1
的定义如下,都是常数计算,没有额外的东西。
1 |
|
所以综上所述,weights1
与weights1Buffer
有种套娃之感,核心数据是一样的。(看一下名字啊,weights1Buffer
=weights1
+buffer
)
d->weights0
与d->weights1Buffer
结构体内的weights0
与weights1Buffer
定义如下。
1 |
|
事实上,weights1Buffer
的调用范围仅限于 Create(),并没有在 GetFrame() 中调用。
Create() 内的同名变量
在 Create() 函数内,有两个临时变量性质的同名变量,定义如下。
1 |
|
它们在定义了d->weights0
与d->weights1Buffer
后被释放。