本文档为 CMU 15-473/673 视觉计算系统课程作业 2:kPhone 469s 图像处理的文档。任务是为备受期待的 kPhone 469s 图像传感器产生的数据实现一个简单的图像处理管线。
项目概述
在作业的第一部分,我们需要处理图像数据以生成尽可能高质量的 RGB 图像。传感器将通过 sensor->ReadSensorData() 方法输出 RAW 数据。结果是一个 Width × Height 的缓冲区。
假设条件
我们将在本项目中做出以下假设。
- 我们假设像素传感器按 Bayer 马赛克排列,即绿色像素的数量是红色或蓝色像素数量的两倍,如下图所示。
具体而言,这是因为人类视觉对绿光最为敏感。
-
像素缺陷(stuck pixels、灵敏度异常的像素)是静态缺陷,在相机拍摄的每张照片中都相同。
-
缺陷行(又称 Beams)应被视为一行缺陷像素,即它们也是静态的。
RAW 图像处理
现在我们可以开始处理图像。确保步骤按正确顺序执行非常重要。缺陷像素校正、Beams、暗角应在 demosaicing 之前去除。噪声滤波、白平衡应在 demosaicing 之后进行。
因此,让我们从缺陷像素去除开始。
缺陷像素去除
我检测缺陷像素的方式是一种硬编码方法。众所周知,缺陷像素会表现出明显偏离的亮度,且在不同图像中保持不变。因此,我们可以通过拍摄黑色场景和灰色场景的照片来找到它们。所有亮度相同的像素都应被视为缺陷像素。
因此,从高层次来看,算法可以概括为:
-
拍摄黑色场景的照片(
black.bin)。 -
拍摄灰色场景的照片(
gray.bin)。 -
找出所有亮度异常的像素(在两个场景中亮度相同)。
-
将它们存储在 map 中。
-
使用相邻的非缺陷像素进行修复。
拍摄黑色场景
我们创建传感器并用像素原始数据填充缓冲区的唯一可靠方法是使用 CreateSensor() 方法。然而,当我们使用 TakePicture() 函数时,我们应该已经拥有黑色场景和灰色场景的输出。
因此,我在类中创建了另一个名为 TakeBlackPicture() 的公共函数。它与 TakePicture() 几乎相同,只是它调用 ProcessBlackshot() 而不是 ProcessShot() 来处理图像——ProcessBlackshot() 是一个简单地将传感器 RAW 数据直接写入图像的函数。
创建缺陷像素 Map
现在,借助灰色图像和黑色图像,我可以轻松定位缺陷像素。首先,我需要在 TakePhoto() 中拍摄这两张图像,因此我更改了此函数的接口为:
Plain Textvoid TakePicture(Image & blackresult, Image & grayresult, Image & result);
类似地,ProcessShot() 也需要接收这两张图像,因此我们也需要更改其接口。
Plain Textvoid ProcessShot(Image & result, Image & blackresult, Image & grayresult, unsigned char * inputBuffer, int w, int h);
一旦我有了这两张图像,接下来需要做的是创建这个缺陷像素 map,并在图像处理管线中动态使用这个 map。
map 的创建很简单,只需比较同一位置的像素。如果它们在黑色图像和灰色图像中表现为相同亮度,则为缺陷像素。
修复缺陷像素
我修复缺陷像素的方法是使用相邻 3×3 像素的盒式窗口的平均值。
Beam 去除
去除 beams 的方式实际上类似于去除像素的过程。我使用黑色图像进行检测:如果某行的平均亮度明显高于整个黑色图像的平均亮度,则应将其标记为缺陷行,即 beam。
补偿 Map
就像缺陷 map 一样,我在补偿 map 中维护 beams。我称之为补偿 map,因为我认为该行中的传感器对光子的响应不足,因此我只需将这部分补回。
修复 Beams
在去除部分,我简单地将亮度增益回补到不足的行。
有趣的是,这种方法修复了大部分 beams,但产生了几个原本图像中不存在的 beams。我无法找出原因,因此最终采用了(可能是最丑陋的)解决方案——硬编码。
Plain Textvoid FixDefectiveRow(int x, int y, unsigned char * inputBuffer, int w, const std::vector<float> &compensationMap)
{
// 省略
if (compensationMap[y] != 0.0) {
if(y != 195 && y != 437 && y != 438 && y != 487 && y != 558 && y != 559 && y != 557 && y != 560)
// 其他部分
}
}
猜怎么着?这确实去除了所有 beams。
暗角去除
demosaicing 之前的最后一步是去除暗角。
我们将通过提升远离图像中心的亮度来去除暗角,增益与距离的幂次成正比。
此函数中的所有参数都表现为魔法数字,是的,它们确实是——这些数字经过多次测试调整,证明能产生相对较好的结果。
Demosaic
现在我们终于可以进行 demosaicing 部分。要获得正确的 demosaic 过程,我们需要了解如何对待 Bayer 马赛克。
算法
基本上,像素传感器被设计为只允许特定颜色的光通过。因此,它们需要按 Bayer 马赛克排列,我们需要对传感器不拥有的另外两个通道进行插值。
例如,通常对于非红色的传感器,我们需要使用其周围四个红色像素对像素的红色通道进行插值。插值方式如下图所示。
这引导我们实现。
白平衡
基本上对于白平衡,我们希望调整 RGB 值的相对强度,使中性色调呈现中性。我进行白平衡的方法是找到图像最亮的区域,并假设其为白色。
请记住,由于白平衡在 demosaic 之后,我们可以将其视为后处理。从现在开始,后处理主导图像处理,这意味着所有函数都以 Image、Width 和 Height 作为输入。
噪声去除
最后,使用中值滤波器,我们将能够过滤图像中的高频噪声,使图像表面更平滑。
中值滤波器
与高斯模糊不同,在中值滤波器中,一个亮像素不会拉高整个区域的平均值。简单来说,我们使用核的中值。
为了优化中值滤波算法,我们应该为每个 RGB 通道维护一个直方图,以便轻松返回中值。
实现
核大小由变量 windowSize 决定。
自动对焦
在作业的后半部分,我需要实现对比度检测自动对焦。
基于对传感器区域的分析(注意 sensor->ReadSensorData() 可以返回完整传感器的裁剪窗口),我需要设计一个算法,通过调用 sensor->SetFocus() 来设置相机的对焦。
设置对焦
基本上,自动对焦是一个自动将对焦设置为表达拍摄效果最佳值的过程——所谓表达,是指我们希望图像主要部分具有最清晰的清晰度。
我的解决方案是为相机设置最小对焦和最大对焦,从最小对焦开始步进,找到对比度最高的拍摄。对比度将通过应用 Sobel 核来确定。
在这部分我们实际上只需要处理 RAW 对比度,因此之前制作的 ProcessBlackshot() 函数可以再次应用。
实现最后一个剩余方法——CalculateImageContrast() 就足够了。
对比度检测
算法
我们将通过使用 Sobel 核计算来确定每张图像的对比度,即
水平方向和
垂直方向。
图像的对比度由每个通道的梯度决定,对于索引 (i,j) ∈ [-1, 1]^2 处的像素,每个通道的梯度由
决定,对比度即为
出于学术诚信原因,本项目的大部分代码将被隐藏。如有任何原因需要访问代码库,请联系 Tony:linghent@andrew.cmu.edu 或 lockbrains@gmail.com。为获得更好的阅读体验,您可以访问链接:https://hackmd.io/@Lockbrains/vcs_2_image_process
