这是 CMU 15-473/673 视觉计算系统课程作业 2:kPhone 469s 图像处理的文档。任务是为备受期待的 kPhone 469s 图像传感器产生的数据实现一个简单的图像处理流水线。
项目概览
在作业的第一部分,我们需要处理图像数据,生成尽可能高质量的 RGB 图像。传感器将通过 sensor->ReadSensorData() 方法输出 RAW 数据。结果是一个 Width 乘以 Height 的缓冲区。
假设
我们在本项目中做出以下假设。
-
我们可以假设像素传感器排列为 Bayer Mosaic,这意味着绿色像素的数量是红色像素或蓝色像素的两倍,如下图所示。
具体来说,这是因为人类视觉对绿光最敏感。 -
像素缺陷(死像素、灵敏度异常的像素)是静态缺陷,对于相机拍摄的每张照片都是相同的。
-
缺陷行(又称 Beams)应被视为一整行缺陷像素,也就是说它们也是静态的。
RAW 图像处理
现在我们可以开始处理图像了。确保按正确的顺序执行步骤非常重要。缺陷像素校正、Beams、Vignetting 应在 demosaicing 之前移除。噪声过滤、白平衡应在 demosaicing 之后进行。
因此,让我们从缺陷像素移除开始。
缺陷像素移除
我检测缺陷像素的方法是硬编码方法。众所周知,缺陷像素表现出明显的亮度偏差,并且在不同的图像中保持不变。因此,我们可以通过拍摄一张黑色场景的照片和一张灰色场景的照片来简单地找到它们。在两个场景中亮度相同的像素应被视为缺陷像素。
因此,从高层次来看,该算法可以总结为:
-
拍摄一张黑色场景的照片 (
black.bin)。 -
拍摄一张灰色场景的照片 (
gray.bin)。 -
找出所有亮度异常的像素(在两个场景中亮度相等)。
-
将它们存储在一个 map 中。
-
使用相邻的非缺陷像素来修复它。
拍摄黑色场景照片
我们创建传感器并用像素原始数据填充缓冲区的唯一可靠方法是使用 CreateSensor() 方法。然而,当我们使用 TakePicture() 函数时,我们应该已经拿到了黑色场景和灰色场景的输出。
因此,我在类中创建了另一个名为 TakeBlackPicture() 的公共函数。它的功能与 TakePicture() 几乎相同,不同之处在于它调用 ProcessBlackshot() 而不是 ProcessShot() 来处理图像——ProcessBlackshot() 只是一个将传感器的 RAW 数据直接写入图像的函数。
创建缺陷像素映射表
现在,借助灰色图像和黑色图像,我可以轻松定位缺陷像素。首先,我需要在 TakePhoto() 中获取这两张图像,因此我将该函数的接口更改为:
C++void TakePicture(Image & blackresult, Image & grayresult, Image & result);
同样地,对于 ProcessShot(),它也需要接收这两张图像,所以我们也需要更改它的接口。
C++void ProcessShot(Image & result, Image & blackresult, Image & grayresult, unsigned char * inputBuffer, int w, int h);
一旦我同时拥有了这两者,接下来的事情就是创建这个缺陷像素映射表,并在图像处理流水线中动态地使用这个映射表。
映射表的创建非常简单,只需比较相同位置的像素。如果它们在黑色图像和灰色图像中显示出相同的亮度,则它是缺陷像素。
修复缺陷像素
我修复缺陷像素的方法只是使用相邻 3x3 像素方框窗口的平均值。

Beam 移除
移除 beam 的方法实际上类似于移除像素的过程。我使用黑色图像进行检测:如果某行的平均亮度明显高于整个黑色图像的平均亮度,则应将其标记为缺陷行,即 beam。
补偿映射表
就像缺陷映射表一样,我在补偿映射表中维护 beams。我称之为补偿映射表,因为我认为该行中的传感器对光子的响应不足,所以我只需将这部分加回去。
修复 Beams
在移除部分,我只是将亮度补偿回响应不足的行。
有趣的是,这种方法修复了大部分 beam,但却产生了几个原本不在图像中的新 beam。我无法找出原因,所以最终采用了(可能是最丑陋的)解决方案——硬编码。
C++void FixDefectiveRow(int x, int y, unsigned char * inputBuffer, int w, const std::vector<float> &compensationMap)
{
// OMIT
if (compensationMap[y] != 0.0) {
if(y != 195 && y != 437 && y != 438 && y != 487 && y != 558 && y != 559 && y != 557 && y != 560)
// Other parts
}
}
你猜怎么着?这真的移除了所有的 beam。
Vignette 移除
在 demosaicing 之前要做的最后一步是移除 vignette。
我们将通过提高远离图像中心区域的亮度来移除 vignette,增益与距离的幂成比例。
这个函数中的所有参数看起来都像魔数,事实也确实如此——这些数字是经过多次测试后调整的,证明它们能产生相对较好的结果。

Demosaic
现在我们终于可以进入 demosaicing 部分了。为了获得正确的 demosaic 过程,我们需要了解如何处理 Bayer Mosaics。
算法
基本上,像素传感器被设计为只允许某些颜色的光通过。因此,它们需要排列成 Bayer Mosaics,我们需要为传感器不具备的其他两个通道进行插值。
通常,例如对于一个非红色传感器,我们需要使用它旁边的四个红色像素来为该像素插值红色通道。插值方法如下图所示。
这引导我们进入实现阶段。
白平衡
基本上,对于白平衡,我们希望调整 RGB 值的相对强度,使中性色调看起来是中性的。我进行白平衡的方法是找到图像中最亮的区域,并假设它是白色的。
请记住,由于白平衡是在 demosaic 之后进行的,我们可以将其视为 Post-Processing。从现在起,后处理主导了图像处理过程,这意味着所有函数都将 Image、Width 和 Height 作为输入。
噪声移除
最后,使用 Median Filter,我们将能够过滤图像中的高频噪声,修饰图像表面,使其更平滑。
Median Filter
与高斯模糊不同,在中值滤波器中,一个亮点像素不会拉高整个区域的平均值。简单来说,我们使用的是内核的中值。
为了优化中值滤波算法,我们应该为每个 RGB 通道维护一个直方图,以便我们可以轻松返回中值。

实现
内核大小由变量 windowSize 决定。
自动对焦
在作业的后半部分,我需要实现反差检测自动对焦。
基于对传感器区域的分析(注意 sensor->ReadSensorData() 可以返回完整传感器的裁剪窗口),我需要设计一种算法,通过调用 sensor->SetFocus() 来设置相机的焦点。
设置焦点
基本上,自动对焦是一个我们 自动 将焦点设置为最能 表现 拍摄效果的过程——所谓表现,是指我们希望在图像的主要部分获得最高的清晰度。
我采用的方案是为相机设置一个最小焦点和最大焦点,并从最小焦点开始步进,找到对比度最高的拍摄效果。对比度将通过应用 Sobel 内核来确定。
在这部分,我们实际上只需要关注 RAW 对比度,因此我们之前制作的 ProcessBlackshot() 函数可以再次应用。
只需要实现最后一个剩余的方法——CalculateImageContrast()。
对比度检测
算法
我们将通过使用 Sobel 内核计算来确定每张图像的对比度,即:
用于水平方向,以及
用于垂直方向。
图像的对比度由每个通道的梯度决定,其中对于索引为 的像素,每个通道的梯度由以下公式确定:
对比度即为:
。
