DehazingCE:基于对比度增强的去雾算法及 VapourSynth 迁移

这一去雾算法在2013年提出,论文见 Optimized contrast enhancement for real-time image and video dehazing,我们称其为 DehazingCE (CE = contrast enhancement)。

算法概述

DehazingCE 基于大气散射模型,分别估计大气光强度和透射率,得到去雾结果。

大气光估计通过图像分块、寻找像素差异最小的子块,以该子块最亮的点作为大气光的估计值。但这里存在一个问题,不知道是我理解有问题,还是我代码迁移有问题,或者算法本身就有问题。如果图像整体偏暗,那寻找到的子块就会很暗,最后以一个像素值很小的点作为大气光估计值,用这个值继续计算,最终结果就是全黑。我对迁移到 VapourSynth 的代码进行测试,确实发现了这一问题。

透射率估计分为粗算和细化两步。粗算是基于大气散射模型进行次数不多的迭代,并自定义像素损失值(大气散射模型的直接计算结果可能会超过 [0, peak],导致像素越界)以决定是否继续迭代。细化通过导向滤波实现。

后处理包括 Gamma 亮度调整和类似均值滤波的均值化计算。前者是避免去雾后图像过暗,后者是避免透射率过低时导致的块效应。在我迁移的代码中,Gamma 亮度调整表现不错,但均值化计算可能是我的代码还有问题,测试结果是不如不做,做了反而凭空多了瑕疵。

代码迁移

代码迁移着实花了很久,满打满算从 4 月份开始读原始代码,到 6 月份完成初版的迁移,几乎花了两个月…原始代码风格很古典,着实有点费力。

从 OpenCV 2 到 OpenCV 3

在最开始,我觉得一下子迁移到 VapourSynth 有点费力,便想尝试着把原始代码先跑起来。原始代码基于 OpenCV 2,存储图像还是用的 IplImage 指针而非 Mat 类。这一部分工作在前面的博文有提到。但折腾好久,也没把原始代码跑起来,最后放弃了,直接迁移。

YUV 还是 RGB

在着手迁移之初,是先支持 YUV 还是先支持 RGB 纠结了好久。其中一部分原因是我还没完全搞懂原始代码的处理流程,从大气光估计部分看,原始代码用的是 RGB 格式,从透射率估计看,原始代码既有只算灰度的(这像是面向 YUV),又有 RGB 三个通道都计算的。最后决定先支持 RGB(因为我先读明白了原始代码的大气光估计部分,沿用了这一部分的 RGB 格式)。

数据结构的转换与基础操作的迁移

确定 RGB 后便开始转换图像数据结构,为减少工作量,尽量不去动原始代码的函数输入输出模式,把 VapourSynth 接口传递过来的图像指针转化为普通指针,送给核心计算函数。在这个过程中,我越来越感觉到 VapourSynth 接口的概念,或者什么叫接口。我渐渐懂得把核心处理剥离开,不再依赖 VapourSynth API 提供的数据类型,而是使用普通指针和数组完成核心计算,在把结果送给 VapourSynth API。

在计算过程中需要进行图像分块、计算图像像素值均值和标准差。在原始代码中,这些操作都是使用的 OpenCV 函数。对于这些并不复杂的操作,我自然不能接受调用第三方库来实现。图像分块最开始是用两层 for 循环赋值,后来才想到用memcpy()函数直接拷贝。计算均值和标准差直接 copy 了一份代码。

完成上述操作后,迁移过程就没什么困难的了,写个集成函数去调用各步计算即可。

VapourSynth API 的一些事情

虽说已经没有大的困难,但具体细节上还是要花时间的。我之前没有写过 ref 参考片段,之前也不知道 VapourSynth 接口没有提供 bool 类型。好在有前辈们写成的其他滤镜,有很多可以参考(copy)的代码。

Debug

指针读取冲突

读取位置 … 时发生访问冲突。

1
2
srcInterleaved[pos] = srcpB[x];
srcInterleaved[pos + 1] = srcpG[x];

这里就出问题是我没有想到的,因为这部分代码之前在AreaResize中已经用过了,没有问题。

回到报错本身,“发生访问冲突”表明指针访问内存时出问题了,指针指向了不属于自己的内存地址,是一个基础的问题。

发现在vs接口部分忘记释放ref,释放之后同样报访问冲突,但这次的报错位置是大气光估计函数的图像分块位置,这才是意料之中(这部分代码最开始写的时候就迷迷糊糊的)。

但另一个引发该错误的原因就很蠢了,假如读入的不是 RGB 格式,而是 YUV 格式,也会引发该错误。这是自然的,YUV 的第二、三个通道的宽高分别是第一个通道的一半,自然会越界。

构建与C++语法

从这次开始我使用了 cmake 进行构建,一开始都是一些技术问题,直到部署 CI 的时候,才通过 cmake 意识到一些 C++ 语法的问题,一些我在很早的时候就看到过但没有读懂的问题。

智能指针

智能指针是 C++11 引入的,但std::make_unique被忘记写入标准了,直到 C++14 才被引入…

noexcept

noexcept在 C++11 引入。

不搞清这些标准问题,在 travis-ci 的编译器上会报错…

高位深支持

高位深支持需要解决两个方面的问题,一是算法本身计算过程的修改,这一部分我改到现在还是有问题…图像暗场处理后会直接变黑…另一是C++语法的问题,带有模板的成员函数怎么写。

语法层面的问题

我之前用模板,借用做饭的话说就是现用现做,而成员函数的声明和定义分开写在两个文件中,这时候再用模板就会出现链接器错误。但把声明和定义写在一起我不太愿意这么搞,于是折中的办法是在 main.cpp 同时 include 含有声明的 .h 和定义的 .cpp 文件。

算法层面的问题

按照惯例,至少对于从 8bit 到 16bit 的扩充,把类似含有 255 的地方改成 peak 就可以了,但这次遇到了不少问题。