TranslateProject/translated/tech/20171002 High Dynamic Range Imaging using OpenCV Cpp python.md

20 KiB
Raw Blame History

使用OpenCVC ++ / Python进行高动态范围HDR成像

在本教程中我们将学习如何使用由不同曝光设置拍摄的多张图像创建高动态范围HDR图像。 我们将以C ++和Python两种形式分享代码。

什么是高动态范围成像?

大多数数码相机和显示器都是按照24位矩阵捕获或者显示彩色图像。 每个颜色通道有8位因此每个通道的像素值在0-255范围内。 换句话说,普通的相机或者显示器的动态范围是有限的。

但是,我们周围世界动态范围极大。 在车库内关灯就会变黑,直接看着太阳就会变得非常亮。 即使不考虑这些极端在日常情况下8位的通道勉强可以捕捉到现场场景。 因此相机会尝试去评估光照并且自动设置曝光这样图像的感兴趣区就会有良好的动态范围并且太暗和太亮的部分会被截取取值为0和255。

在下图中,左侧的图像是正常曝光的图像。 请注意,由于相机决定使用拍摄主体(我的儿子)的设置,所以背景中的天空已经完全流失了,但是明亮的天空也因此被刷掉了。 右侧的图像是由iPhone生成的HDR图像。

High Dynamic Range (HDR)

iPhone是如何拍摄HDR图像的呢 它实际上采用三种不同的曝光度拍摄了3张图像3张图像拍摄非常迅速在3张图像之间几乎没有产生位移。然后组合三幅图像来产生HDR图像。 我们将在下一节看到一些细节。

将在不同曝光设置下获取的相同场景的不同图像组合的过程称为高动态范围HDR成像。

高动态范围HDR成像是如何工作的

在本节中我们来看下使用OpenCV创建HDR图像的步骤。

要想轻松学习本教程,请点击此处下载C ++和Python代码还有图像。 如果您有兴趣了解更多关于人工智能,计算机视觉和机器学习的信息,请订阅我们的电子杂志。

第1步捕获不同曝光度的多张图像

当我们使用相机拍照时每个通道只有8位来表示场景的动态范围亮度范围。 但是,通过改变快门速度,我们可以在不同的曝光条件下拍摄多个场景图像。 大多数单反相机SLR有一个功能称为自动包围式曝光AEB只需按一下按钮我们就可以在不同的曝光下拍摄多张照片。 如果你正在使用iPhone你可以使用这个自动包围式HDR应用程序如果你是一个Android用户你可以尝试一个更好的相机应用程序

场景没有变化时,在相机上使用自动包围式曝光或在手机上使用自动包围式应用程序,我们可以一张接一张地快速拍摄多张照片。 当我们在iPhone中使用HDR模式时会拍摄三张照片。

  1. 曝光不足的图像:该图像比正确曝光的图像更暗。 目标是捕捉非常明亮的图像部分。
  2. 正确曝光的图像:这是相机将根据其估计的照明拍摄的常规图像。
  3. 曝光过度的图像:该图像比正确曝光的图像更亮。 目标是拍摄非常黑暗的图像部分。

但是如果场景的动态范围很大我们可以拍摄三张以上的图片来合成HDR图像。 在本教程中我们将使用曝光时间为1/30秒0.25秒2.5秒和15秒的4张图像。 缩略图如下所示。

Auto Exposure Bracketed  HDR image sequence

单反相机或手机的曝光时间和其他设置的信息通常存储在JPEG文件的EXIF元数据中。 查看此链接可查看Windows和Mac中存储在JPEG文件中的EXIF元数据。 或者,您可以使用我最喜欢的名为EXIFTOOL的查看EXIF的命令行工具。

我们先从读取分配到不同曝光时间的图像开始

C++

void readImagesAndTimes(vector<Mat> &images, vector<float> &times)
{
   
  int numImages = 4;
 
  // 曝光时间列表
  static const float timesArray[] = {1/30.0f,0.25,2.5,15.0};
  times.assign(timesArray, timesArray + numImages);
   
  // 图像文件名称列表
  static const char* filenames[] = {"img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"};
  for(int i=0; i < numImages; i++)
  {
    Mat im = imread(filenames[i]);
    images.push_back(im);
  }
 
}

Python

def readImagesAndTimes():
  # 曝光时间列表
  times = np.array([ 1/30.0, 0.25, 2.5, 15.0 ], dtype=np.float32)
   
  # 图像文件名称列表
  filenames = ["img_0.033.jpg", "img_0.25.jpg", "img_2.5.jpg", "img_15.jpg"]
  images = []
  for filename in filenames:
    im = cv2.imread(filename)
    images.append(im)
   
  return images, times

第2步对齐图像

合成HDR图像时使用的图像如果未对齐可能会导致严重的伪影。 在下图中左侧的图像是使用未对齐的图像组成的HDR图像右侧的图像是使用对齐的图像的图像。 通过放大图像的一部分,使用红色圆圈显示的,,我们会在左侧图像中看到严重的鬼影。

Misalignment problem in HDR

在拍摄照片制作HDR图像时专业摄影师自然是将相机安装在三脚架上。 他们还使用称为镜像锁定功能来减少额外的振动。 即使如此,图像可能仍然没有完美对齐,因为没有办法保证无振动的环境。 使用手持相机或手机拍摄图像时,对齐问题会变得更糟。

幸运的是OpenCV提供了一种简单的方法使用AlignMTB对齐这些图像。 该算法将所有图像转换为中值阈值位图MTB。 图像的MTB生成方式为将比中值亮度的更亮的分配为1其余为0。 MTB不随曝光时间的改变而改变。 因此不需要我们指定曝光时间就可以对齐MTB。

基于MTB的对齐方式的代码如下。

C++

// 对齐输入图像
Ptr<AlignMTB> alignMTB = createAlignMTB();
alignMTB->process(images, images);

Python

# 对齐输入图像
alignMTB = cv2.createAlignMTB()
alignMTB.process(images, images)

第3步提取相机响应函数

典型相机的响应与场景亮度不成线性关系。 那是什么意思呢? 假设有两个物体由同一个相机拍摄,在现实世界中其中一个物体是另一个物体亮度的两倍。 当您测量照片中两个物体的像素亮度时,较亮物体的像素值将不会是较暗物体的两倍。 在不估计相机响应函数CRF的情况下我们将无法将图像合并到一个HDR图像中。

将多个曝光图像合并为HDR图像意味着什么

只考虑图像的某个位置xy一个像素。 如果CRF是线性的则像素值将直接与曝光时间成比例除非像素在特定图像中太暗即接近0或太亮即接近255。 我们可以过滤出这些不好的像素(太暗或太亮),并且将像素值除以曝光时间来估计像素的亮度,然后在像素不差的(太暗或太亮)所有图像上对亮度值取平均。我们可以对所有像素进行这样的处理,并通过对“好”像素进行平均来获得所有像素的单张图像。

但是CRF不是线性的 我们需要评估CRF把图像强度变成线性然后才能合并或者平均它们。

好消息是如果我们知道每个图像的曝光时间则可以从图像估计CRF。 与计算机视觉中的许多问题一样找到CRF的问题本质是一个最优解问题其目标是使由数据项和平滑项组成的目标函数最小化。 这些问题通常会降维到线性最小二乘问题这些问题可以使用奇异值分解SVD来解决奇异值分解是所有线性代数包的一部分。 CRF提取算法的细节在从照片提取高动态范围辐射图这篇论文中可以找到。

使用OpenCv的CalibrateDebevec 或者CalibrateRobertson就可以用2行代码找到CRF。本篇教程中我们使用 CalibrateDebevec

C++

// 获取图像响应函数 (CRF)
Mat responseDebevec;
Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
calibrateDebevec->process(images, responseDebevec, times);

Python

# 获取图像响应函数 (CRF)
calibrateDebevec = cv2.createCalibrateDebevec()
responseDebevec = calibrateDebevec.process(images, times)

下图显示了使用红绿蓝通道的图像提取的CRF。

Camera Response Function

第4步合并图像

一旦CRF评估结束我们可以使用MergeDebevec将曝光图像合并成一个HDR图像。 C ++和Python代码如下所示。

C++

// 将图像合并为HDR线性图像
Mat hdrDebevec;
Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();
mergeDebevec->process(images, hdrDebevec, times, responseDebevec);
// 保存图像
imwrite("hdrDebevec.hdr", hdrDebevec);

Python

# 将图像合并为HDR线性图像
mergeDebevec = cv2.createMergeDebevec()
hdrDebevec = mergeDebevec.process(images, times, responseDebevec)
# 保存图像
cv2.imwrite("hdrDebevec.hdr", hdrDebevec)

上面保存的HDR图像可以在Photoshop中加载并进行色调映射。示例图像如下所示。

HDR Photoshop tone mapping HDR Photoshop 色调映射

第5步色调映射

现在我们已经将我们的曝光图像合并到一个HDR图像中。 你能猜出这个图像的最小和最大像素值吗? 对于黑色条件最小值显然为0。 理论最大值是什么? 无限大! 在实践中,不同情况下的最大值是不同的。 如果场景包含非常明亮的光源,那么最大值就会非常大。

尽管我们已经使用多个图像恢复了相对亮度信息但是我们现在又面临了新的挑战将这些信息保存为24位图像用于显示。

将高动态范围HDR图像转换为8位单通道图像的过程称为色调映射。这个过程的同时还需要保留尽可能多的细节。

有几种色调映射算法。 OpenCV实现了其中的四个。 要记住的是没有一个绝对正确的方法来做色调映射。 通常,我们希望在色调映射图像中看到比任何一个曝光图像更多的细节。 有时色调映射的目标是产生逼真的图像,而且往往是产生超现实图像的目标。 在OpenCV中实现的算法倾向于产生现实的并不那么生动的结果。

我们来看看各种选项。 以下列出了不同色调映射算法的一些常见参数。

  1. 伽马gamma该参数通过应用伽马gamma校正来压缩动态范围。 当gamma等于1时不应用修正。 小于1的伽玛会使图像变暗而大于1的伽马会使图像变亮。
  2. 饱和度saturation该参数用于增加或减少饱和度。 饱和度高时,色彩更丰富,更浓。 饱和度值接近零,使颜色逐渐消失为灰度。
  3. 对比度contrast控制输出图像的对比度即log (maxPixelValue/minPixelValue))。

让我们来探索OpenCV中可用的四种色调映射算法。

Drago 色调映射

Drago 色调映射的参数如下所示

createTonemapDrago
(
float   gamma = 1.0f,
float   saturation = 1.0f,
float   bias = 0.85f 
)   

这里bias是[01]范围内偏差函数的值。 从0.7到0.9的值通常效果较好。 默认值是0.85。 有关更多技术细节,请参阅这篇论文

C ++和Python代码如下所示。 参数是通过反复试验获得的。 最后的结果乘以3只是因为它给出了最令人满意的结果。

C++

// 使用Drago色调映射算法获得24位彩色图像
Mat ldrDrago;
Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);
tonemapDrago->process(hdrDebevec, ldrDrago);
ldrDrago = 3 * ldrDrago;
imwrite("ldr-Drago.jpg", ldrDrago * 255);

Python

# 使用Drago色调映射算法获得24位彩色图像
tonemapDrago = cv2.createTonemapDrago(1.0, 0.7)
ldrDrago = tonemapDrago.process(hdrDebevec)
ldrDrago = 3 * ldrDrago
cv2.imwrite("ldr-Drago.jpg", ldrDrago * 255)

结果如下

HDR tone mapping using Drago's algorithm 使用Drago算法的HDR色调映射

Durand 色调映射

Durand 色调映射的参数如下所示

createTonemapDurand 
(   
  float     gamma = 1.0f, 
  float     contrast = 4.0f,
  float     saturation = 1.0f,
  float     sigma_space = 2.0f,
  float     sigma_color = 2.0f 
); 

该算法基于将图像分解为基础层和细节层。 使用称为双边滤波器的边缘保留滤波器来获得基本层。 sigma_space和sigma_color是双边滤波器的参数分别控制空间域和彩色域中的平滑量。

有关更多详细信息,请查看这篇论文

C++

// 使用Durand色调映射算法获得24位彩色图像
Mat ldrDurand;
Ptr<TonemapDurand> tonemapDurand = createTonemapDurand(1.5,4,1.0,1,1);
tonemapDurand->process(hdrDebevec, ldrDurand);
ldrDurand = 3 * ldrDurand;
imwrite("ldr-Durand.jpg", ldrDurand * 255);

Python

# 使用Durand色调映射算法获得24位彩色图像
 tonemapDurand = cv2.createTonemapDurand(1.5,4,1.0,1,1)
 ldrDurand = tonemapDurand.process(hdrDebevec)
 ldrDurand = 3 * ldrDurand
 cv2.imwrite("ldr-Durand.jpg", ldrDurand * 255)

结果如下

HDR tone mapping using Durand's algorithm 使用Durand算法的HDR色调映射

Reinhard 色调映射


createTonemapReinhard
(
float   gamma = 1.0f,
float   intensity = 0.0f,
float   light_adapt = 1.0f,
float   color_adapt = 0.0f 
)   

intensity 参数应在[-8, 8]范围内。 更高的亮度值会产生更明亮的结果。 light_adapt控制灯光范围为[0, 1]。 值1表示仅基于像素值的自适应而值0表示全局自适应。 中间值可以用于两者的加权组合。 参数color_adapt控制色彩范围为[0, 1]。 如果值被设置为1则通道被独立处理如果该值被设置为0则每个通道的适应级别相同。中间值可以用于两者的加权组合。

有关更多详细信息,请查看这篇论文

C++

// 使用Reinhard色调映射算法获得24位彩色图像
Mat ldrReinhard;
Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0,0,0);
tonemapReinhard->process(hdrDebevec, ldrReinhard);
imwrite("ldr-Reinhard.jpg", ldrReinhard * 255);

Python

# 使用Reinhard色调映射算法获得24位彩色图像
tonemapReinhard = cv2.createTonemapReinhard(1.5, 0,0,0)
ldrReinhard = tonemapReinhard.process(hdrDebevec)
cv2.imwrite("ldr-Reinhard.jpg", ldrReinhard * 255)

结果如下

HDR tone mapping using Reinhard's algorithm 使用Reinhard算法的HDR色调映射

Mantiuk 色调映射

createTonemapMantiuk
(   
float   gamma = 1.0f,
float   scale = 0.7f,
float   saturation = 1.0f 
)   

参数scale是对比度比例因子。 从0.7到0.9的值通常效果较好

有关更多详细信息,请查看这篇论文

C++

// 使用Mantiuk色调映射算法获得24位彩色图像
Mat ldrMantiuk;
Ptr<TonemapMantiuk> tonemapMantiuk = createTonemapMantiuk(2.2,0.85, 1.2);
tonemapMantiuk->process(hdrDebevec, ldrMantiuk);
ldrMantiuk = 3 * ldrMantiuk;
imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255);

Python

# 使用Mantiuk色调映射算法获得24位彩色图像
tonemapMantiuk = cv2.createTonemapMantiuk(2.2,0.85, 1.2)
ldrMantiuk = tonemapMantiuk.process(hdrDebevec)
ldrMantiuk = 3 * ldrMantiuk
cv2.imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255)

结果如下

HDR tone mapping using Mantiuk's algorithm 使用Mantiuk算法的HDR色调映射

订阅然后下载代码

如果你喜欢这篇文章并希望下载本文中使用的代码C ++和Python和示例图片订阅我们的电子杂志。 您还将获得免费的计算机视觉资源指南。 在我们的电子杂志中我们分享了用C ++还有Python编写的OpenCV教程和例子以及计算机视觉和机器学习的算法和新闻。

点此订阅

图像学分

本文中使用的四个曝光图像获得CC BY-SA 3.0许可,并从维基百科的HDR页面下载。 图像由Kevin McCoy拍摄。


作者简介:

我是一位热爱计算机视觉和机器学习的企业家,拥有十多年的实践经验(还有博士学位)。

2007年在完成博士学位之后我和我的顾问David Kriegman博士还有Kevin Barnes共同创办了TAAZ公司.。 我们的计算机视觉和机器学习算法的可扩展性和鲁棒性已经经过了试用了我们产品的超过1亿的用户的严格测试。


via: http://www.learnopencv.com/high-dynamic-range-hdr-imaging-using-opencv-cpp-python/

作者:SATYA MALLICK 译者:Flowsnow 校对:校对者ID

本文由 LCTT 原创编译,Linux中国 荣誉推出