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后被释放。