记一次从C语言到C++的迁移——初次接触VapourSynth API
引言
想在VapourSynth中实现Jinc(EWA Lanczos)放大算法,已经有一份实现,https://github.com/Lypheo/EWA-Resampling-VS 。但这份代码仅支持整型16bit输入(严格来说是仅支持整型9~16bit输入,作者说仅支持16bit RGB输入,但无论看代码还是实际测试,16bit YUV和10bit YUV都是支持的)。
所以我想在原作者代码的基础上,先完善上述输入问题。至少要支持整型8bit YUV输入,后续再考虑支持32bit浮点数输入。
阅读了原作者代码和其他一些较为成熟的基于VapourSynth实现的代码,大致明白了VapourSynth API调用的过程和基础的代码结构。至少对于Jinc算法,在整型8-16bit的范围内,支持的输入类型仅取决于关键变量的类型定义(这话看起来是一句正确的废话,要怪就怪C++是一门强类型语言Orz)。
再具体一点,支持8bit需要使用uint8_t
来定义关键变量,支持9~16bit需要使用unit16_t
来定义关键变量。所以要在代码中,根据输入视频的位深进行类型转换。
在C语言中,我能想到的就是使用宏来完成上述过程。但是复杂一点宏我就不会写…而且宏也不安全…我还是用C++的模板来写吧。看了一下不同前辈们的代码,也都是用模板来写的,方便Copy。
准备工作
不着急写模板,从C到C++,代码还是要改一下的。
在C++中,使用
uint8_t
等类型需要include<cstdint>
头文件。在C++使用圆周率
M_PI
的宏,要include相应的数学头文件,随后又带来了新的问题,所以直接定义一个宏吧。内存控制方面,在C++中使用
malloc()
分配内存,需要进行强制类型转换。具体到这份代码,将1
data = malloc(sizeof(d));
修改为
1
data = (FilterData *)malloc(sizeof(d));
其中,
FilterData
是自定义类型。
理解VapourSynth API和VapourSynth插件的基础代码
理解VapourSynth API
解决了基础的代码语法问题,还需要理解VapourSynth API,起码要明白怎么读写视频、怎么获取视频格式信息、视频处理前后储存在什么变量中、这些变量是怎么声明的。
借助VapourSynth API,上述问题的解答如下。
- 视频的读取写入通过
vsapi->getReadPtr
、vsapi->getWritePtr
、vsapi->newVideoFrame
实现。 - 视频格式信息通过
d->vi->format
获取,具体信息对应具体函数,如获取位深使用bytesPerSample
;视频尺寸信息通过vsapi->getFrameHeight
、vsapi->getFramewidth
获取。 - 储存视频的变量类型是
VSFrameRef*
(通过指针储存),分平面储存;输入视频要加上const
限制。 - 好像3已经回答了变量声明的问题。
- 顺带一提,读取用户端输入的函数变量通过类似
vsapi->propGetFloat
的方式完成。
VapourSynth的Hello World
写一个VapourSynth插件,或者说写一个基于VapourSynth框架的算法实现,要包含这么几个部分。
1 |
|
代码主体的四个函数中,filterInit()
和filterFree()
是辅助性质的函数,主要代码在filterGetFrame()
和filterCreate()
中。其中filterCreat()
多是用来获取用户端的输入参数,所以实现算法的核心代码一般位于filterGetFrame()
中。
写模板
Coding过程
语法和API调用都搞明白了,下面开始写模板。具体需求在引言部分已经写清楚了。现在回到原作者的代码上,寻找鲁棒性缺失的地方。
容易发现,在GetFrame
部分,直接使用uint8_t
和uint16_t
声明了储存视频的变量。
1 |
|
1 |
|
这就是原代码仅支持9~16bit的原因(之一),要用函数模板替代上述直接声明。
当年上C++课的时候,老师说模板不考…所以在教材目录上画个叉,模板的学习到此为止…写模板倒容易写,像定义函数那样照猫画虎。但怎么调用模板,花了一段时间才弄明白。
实现引言中的需求,逻辑很简单,伪代码如下。
1 |
|
我之所以不知道怎么调用函数模板,是因为我一开始不想改动太多的原始代码。原始代码用uint16_t
直接声明变量,我也想借助函数模板直接做类似的事情。于是,我的代码写成了下面这个样子。
1 |
|
我想当然的以为通过if语句,就可以让代码根据视频输入格式选择变量类型,但我忘了一件事情…if语句内的变量是局部变量,作用域仅限if语句内…
好吧…那我把后面的代码分别往if和else内复制一遍,直接在if语句内处理到底…这也太蠢了。
坦白地讲,我心里很清楚,这就是半路出家自学和科班的差距。
看了前辈的代码,将函数模板写成void函数,在GetFrame
部分仅进行函数模板的调用,处理的代码均写在函数模板内。把该写的框架都写全,代码如下。
1 |
|
至此,完成了模板的撰写(及小小的代码重构)和调用,测试一下,8bit、10bit、16bit YUV格式输入都没有问题。
值得注意的基础知识(2019.12.18)
上面这个模板是从前辈的一处代码中照猫画猫得到的(当时,模板函数的声明应该是copy过来的,改了一下,没太动脑子)(照着模板写模板,禁止套娃),有些细节没注意,或者现在忘了,来记一下。
noexcept
关键字与异常处理、编译优化
在模板函数的函数声明和函数体之间(上述代码的第2行),有这么一个关键字,noexcept
。
noexcept
是C++11引入的新特性(我居然能注意不同的C++标准了,以前觉得这是要学很久才能去学的东西Orz)。根据这篇博文,C++11 带来的新特性 (3)—— 关键字noexcept,noexcept
会告诉编译器,使用了noexcept
的函数不会发生异常。由于异常处理是在运行而非编译时执行,所以异常处理会影响编译器的优化,通过noexcept
这么一“声明”,便于编译器做更多的优化。
顺带一提,在引入noexcept
关键字之前,是使用throw()
来实现类似功能。
博文中还有更深入的内容,以后还值得拿出来再看。
两个const
我第一次注意到const FilterData* const VS_RESTRICT d
这样的声明,有点惊呆了,怎么有两个const
。
总结
总结一下,这篇博文的标题是从C语言到C++的迁移。就我目前的感受,C语言和C++的区别体现在C++有模板,而且对类型转换更加严格。
另外,文章用相当长的篇幅描述了VapourSynth的C++ API,我搞明白这些东西花了挺长一段时间,即使只算“开窍”后有明显进步的时间,也有一个多星期。如果现在让我从头用VapourSynth API写一个插件,照猫画虎应该没什么大问题,而且通过这段时间对C++的语法也有了更多的了解。
但另一方面,对于算法实现,这个真要花更多时间。我之前只知道Jinc的数学公式,知道怎样“手动推演”实现视频放大,但完全不知道在代码实现时,还有反向映射这一回事。在代码实现中,除了定义Jinc函数的部分,其余都是简单的加减乘除四则运算。关于8bit、16bit位深处理的地方,也没有涉及数学运算,区别只是变量的声明。这些东西,都是从算法到代码要学习的东西。同时,代码的异常处理、鲁棒性,也真实地接触到了。
之后的工作,具体到这份代码,就是实现taps功能。这份代码之外,我希望能够从头实现一个算法。
后记
关于编译后运行速度问题的。
关于鲁棒性,能否接受灰度视频/单平面视频输入。
C++与C头文件的区别,使用std库是否能提高速度,内存分配时的类型转换是否是降低速度的主因?
代码重构的预备
测试了半个下午,想解决dll运行慢的问题。(我实在不能理解也无法接受作者提供的dll速度是1s/帧,到我这就是9s/帧)。
- 不是编译器配置的锅,我自行编译了Deblock,速度很快,虽然我的编译器配置与原作者的并不完全相同。
- 又重复了一次,修改预处理后,除了
malloc()
函数部分,其他与作者代码保持一致,仍然是9s/帧的速度。不是我添加了函数模板导致的速度变慢。 - 换用gcc编译器…一时半会还没折腾好,先不用了..
- 我现在真的是无法理解,作者的
data = malloc(sizeof(d));
是怎么编译通过的,我目前查到的所有资料,都说在malloc()
函数前需要声明变量类型。 - 我现在还担心,等我代码重构了,还是慢怎么办…