记一次从 AviSynth 到 VapourSynth 的迁移(3):找不到的 Bug 与代码优化
回忆重连
离写这个系列的上一篇 Blog 已经一个月有余,离发布 VapourSynth 版 AreaResize 也三个星期了。一直没有继续写 Blog,有些东西都要忘了。这个时候发现 commit 表情还是挺有用的。
从功能上讲,上一篇 Blog 写到(基于原始 avs 版)修正了颜色问题,也在一定程度上支持高位深输入;但目标尺寸仍必须是“常规”的尺寸,比如 960x540 这样的,在一些稍“不常规”的尺寸上(如 1280x900),8bit 会分屏,32bit 会奔溃。
总之问题集中在尺寸上,尝试好久,包括调换垂直方向与水平方向的处理顺序,以及其他一些细节的尝试,都没有帮助。当时为了找这个 bug,盯着屏幕盯了好像有两三天,也没有结果。
最后没办法, 因为 AreaResize 除了原始的 avs 版,还有一个优化过的 avs 版。优化版作者的本意的加快处理速度,但我把代码迁移到 vs 后,发现上面的 Bug 居然解决了——虽然我真的看不出,两份代码除了速度优化外,其他方面的区别。
神奇的优化
如前面所说,我并没有发现原始 avs 版代码和优化 avs 版代码有除了速度优化之外的区别,但真的迁移了优化版代码后,上面的 Bug 就解决了。
具体的分析,等我看明白了再写吧。代码改到这里,排查了手滑写错了一个循环,除了 700x700 这样奇怪格式外,对于 YUV 格式,在高位深、一般目标尺寸输入上都没有问题了。下面就是支持 RGB 的问题。
在 1 月 2 号晚上和 1 月 3 号一天,都在解决支持 RGB 的问题,其实也就是在学相关的概念和数据结构。如同我在某个 commit 下面的评论中所写:“让我开始阅读 VapourSynth 的源码和官方 plugins 示例(就是 vscore.cpp 中的那些函数)。让我对 vs api 与框架有了近乎全面的认识,至少知道从头到尾是怎么计算的。”其中,数据结构的相关内容写在了这篇 Blog 中。
支持 RGB
写一写支持 RGB 的代码实现。
对于 YUV 格式,写一个循环,依次处理三个平面/通道即可。但我这么套在 RGB 上,不行,崩溃(但其实奔溃还有数据类型的问题,一开始的代码有整型处理,遇到浮点数就崩了)。让后又试着按 stack 格式写了代码,不循环,一个像素点依次处理三个通道,再移动到下一个像素点。像下面这样
1 |
|
话说这改后,好像也是崩溃(或者是在关闭预览窗口后奔溃),所以放弃了,老老实实地(在process()
函数中)先转换为 Interleaved 结构,在送到具体的Ver()
中处理。
1 |
|
至此,数据结构的问题已经解决,但对于 RGB 输入仍不能正常处理。
然后修正了一个蛋疼的 Bug:我在写peak
的时候,把1 << bitsPerSample
写成了1 << sampleType
,所以一直有问题、崩溃。改正之后,8bit RGB 输入终于能看了。而 16bit 和 32bit 由于查表的范围问题,还会崩溃。
然后将gamma_LUT
声明为 double。到这里,输出结果是图像中的一部分带有蓝点红点绿点(提高gamma_LUT
的精度,在单个帧中,会让这些点变少,而正常图像的部分增多)。
顺带,说一个 C++ 的基础知识,对于auto
声明,应该是确定了后就不能改了,而不能在不同变量类型间反复横跳。
修正查表(LUT)的相关问题
查表,即 Look up table,在代码中一般缩写为 LUT。
论数组与指针的区别
1 月 12 号解决了上述(8bit)下图像带有红点绿点的问题。解决方法是把gamma_LUT
的声明由数组变成指针。(开发版 commit:a5dc85d
)
1 |
|
至于为什么从数组变到指针就没有问题了,这个…我猜是数组一些地方溢出了?但我尝试过加[0, 255]
限制,也不能彻底解决啊。
增加 16bit 支持
这个更偏向于体力活,没有太多要写的。只是需要明白一个事情,在 8bit 处理中用到的宏常量RGB_PIXEL_RANGE_EXTENDED
,是为了让 8bit 的计算不那么僵硬,增加中间数据的过渡,就和我们要把8bit的图像素材扩展到 10bit 或 16bit 处理是一个道理。这里定义的宏常量值为25501
,相当于把 8bit 的计算范围扩大了 100 倍。
事实上,log2(25501)
介于 14 与 15 之间。所以对于喂给 AreaResize 的 RGB 输入,只有 16bit(和一般不会有人用的 15bit)能实现比 8bit 更“精细”的处理。(但话说回来,就算 8bit 的中间数据再精细,终归还要化为 8bit,所以 10bit 的优势还是在的?)
增加 32bit 支持与gamma
参数
1 月 13 号做的事情包括增加 16bit 与 32bit 支持,增加gamma
参数。
先思考一下为要查表。就 AreaResize 而言,由于对 RGB 格式增加了 Gamma 校正(但我不太明白原作者为什么不给 YUV 格式增加 Gamma 校正?是一般的滤镜都不做的惯例吗?),计算量增大,不如把 Gamma 校正后的颜色值用查表的形式给出。
但是,对 32bit 浮点数数据,由于数据过于精细,计算表格本身的计算量过大,已经不适合再查表了。而且查表的初衷是为了做 Gamma 校正,而无论是我查的一些资料,还是我的实际测试,在 32bit 下,由于精度足够高,做不做 Gamma 校正都没什么区别。
所以在 32bit 的处理上,没有加 Gamma 校正,也没有做成查表的形式,直接是用到的时候再计算。
另外,给 8~16bit RGB 增加了 Gamma 校正参数,让使用者能够自行调整 Gamma 校正的数值。因为 Gamma 校正具体用什么值,本身似乎就是需要商榷的。
至此,面上的问题都解决了。然后整理了一下代码,就发布了。
未来要做的
bool 型的函数
其实我在迁移的过程中,一直都回避了一个问题。在 avs 版代码中,核心处理函数Ver()
和Hor()
都是bool型,在调用过程中会借此给一些特殊情况单独处理,以增强鲁棒性(在最开始,我以为这个判断是防止内存溢出的)。但我为了快点迁移代码,把这个问题暂时忽略了。
代码优化了什么
虽然说,优化前后的两版 avs 代码,都不难读懂,但这种优化的思路,值得我去学习。这是把实际项目和算法题联系起来的桥梁。况且,为什么换成优化后的代码,就没有“挑剔输入尺寸”的 Bug,这个问题我还不知道。
一些优化
没什么用的指令集优化
在 AreaResize 中,接受 8bit 和 16bit RGB 输入时,要进行查表操作。联想到 JincResize 的经验,能否在计算 table 的时候进行指令集优化,以加快速度。
于是风风火火写了相关代码。但代码写完,冷静下来才注意到,AreaResize 所需的表格很简单。相比 JincResize 中由两个向量(数组)得到 table,AreaResize 中计算 table 仅需一些算数乘除法,好像不需要我再专门优化,因为由于很简单…或许编译器本身就给优化了?
特意进行了稍长一点的测试,发现 8bit 下优化之后的代码稍快,而 16bit 则是优化前的稍快。所以…好像没什么必要吧。
或许画蛇添足的 Parallel
因为偶然看到《OpenCL 2.0 异构计算》这本书,读到了 C++ AMP(已经成为 C++11 标准的一个扩展),明白了parallel_for
的含义,可以简单理解为将 for 循环并行化。
由于有不同的实现方式,所以多少让人感到凌乱。这里用了微软的ppl.h
(已经整合到了 msvc 中),至于 gcc…先放着吧…Orz
1 |
|
测试发现加了上述优化后,速度反而降了…Orz…然后我才注意到,VapourSynth 本身已经有fmParallel
参数,应该是已经包含了 Parallel 相关优化?所以我这样做是画蛇添足?
目前 Github 上 repo 的情况,Parallel 部分提交到了主分支,而 AVX 部分则是提交到了新建的 dev 分支。
总之,这应该是我走出前人已有算法,做出属于我的微小贡献的第一步吧。