当前位置:网站首页>Opencv学习笔记 高动态范围 (HDR) 成像
Opencv学习笔记 高动态范围 (HDR) 成像
2022-07-07 18:03:00 【坐望云起】
一、什么是高动态范围 (HDR) 成像
组合在不同曝光设置下获取的同一场景的不同图像的过程称为高动态范围 (HDR)成像。
大多数数码相机和显示器将彩色图像捕获或显示为 24 位矩阵。每个颜色通道有 8 位,因此每个通道的像素值在 0 – 255 范围内。换句话说,普通相机或显示器具有有限的动态范围。
然而,我们周围的世界有着非常大的动态范围。车库里关灯时它会变得漆黑一片,如果你直视太阳,它会变得非常亮。即使不考虑这些极端情况,在日常情况下,8 位也勉强足以捕捉场景。因此,相机会尝试估计光照并自动设置曝光,以使图像中最有趣的部分具有良好的动态范围,并且将太暗和太亮的部分分别裁剪为 0 和 255。
在下图中,左边的图像是正常曝光的图像。请注意,背景中的天空完全被洗掉了,因为相机决定使用正确拍摄对象的设置,但明亮的天空被洗掉了。右图是 iPhone 制作的 HDR 图像。
iPhone 如何捕捉 HDR 图像?它实际上在三种不同的曝光下拍摄了 3 张图像。图像是连续快速拍摄的,因此三张照片之间几乎没有移动。然后将这三个图像组合起来以生成 HDR 图像。我们将在下一节中看到详细信息。
二、高动态范围 (HDR) 成像如何工作?
在这里将介绍使用 OpenCV 创建 HDR 图像的步骤。
第 1 步:捕获具有不同曝光度的多张图像
当我们使用相机拍照时,我们每个通道只有 8 位来表示场景的动态范围(亮度范围)。但是我们可以通过改变快门速度在不同曝光下拍摄多张场景图像。大多数单反相机都有一个称为自动包围曝光 (AEB) 的功能,只需按一下按钮,我们就可以在不同的曝光下拍摄多张照片。如果您使用的是 iPhone,则可以使用此AutoBracket HDR 应用程序,如果您是 android 用户,则可以尝试A Better Camera 应用程序。
使用相机上的 AEB 或手机上的自动包围式应用程序,我们可以一张一张地快速拍摄多张照片,这样场景就不会改变。当我们在 iPhone 中使用 HDR 模式时,它会拍摄三张照片。
1、曝光不足的图像:此图像比正确曝光的图像更暗。目标是捕获图像中非常明亮的部分。
2、正确曝光的图像:这是相机根据其估计的照度拍摄的常规图像。
3、曝光过度的图像:此图像比正确曝光的图像更亮。目标是捕捉图像中非常暗的部分。
但是,如果场景的动态范围非常大,我们可以拍摄三张以上的图片来合成 HDR 图像。在本教程中,我们将使用 4 张曝光时间分别为 1/30、0.25、2.5 和 15 秒的图像。缩略图如下所示。
单反相机或手机使用的有关曝光时间和其他设置的信息通常存储在 JPEG 文件的 EXIF 元数据中。查看此链接以查看存储在 Windows 和 Mac 中的 JPEG 文件中的 EXIF 元数据。或者,您可以使用我最喜欢的 EXIF 命令行实用程序EXIFTOOL。
让我们从读取图像开始,分配曝光时间
C++
void readImagesAndTimes(vector<Mat> &images, vector<float> ×)
{
int numImages = 4;
// List of exposure times
static const float timesArray[] = {1/30.0f,0.25,2.5,15.0};
times.assign(timesArray, timesArray + numImages);
// List of image filenames
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():
# List of exposure times
times = np.array([ 1/30.0, 0.25, 2.5, 15.0 ], dtype=np.float32)
# List of image filenames
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 图像,右边的图像是使用对齐的图像合成的图像。通过放大图像的一部分,使用红色圆圈显示,我们在左侧图像中看到了严重的重影伪影。
当然,在拍摄用于创建 HDR 图像的照片时,专业摄影师会将相机安装在三脚架上。他们还使用称为镜子锁定的功能来减少额外的振动。即使这样,图像也可能无法完美对齐,因为无法保证无振动的环境。当使用手持相机或手机拍摄图像时,对齐问题会变得更糟。
幸运的是,OpenCV 提供了一种使用AlignMTB. 该算法将所有图像转换为中值阈值位图 (MTB)。图像的 MTB 是通过将值 1 分配给比中值亮度更亮的像素,否则为 0 来计算的。MTB 对曝光时间是不变的。因此,MTB 可以对齐,而无需我们指定曝光时间。
使用以下代码行执行基于 MTB 的对齐。
C++
// Align input images
Ptr<AlignMTB> alignMTB = createAlignMTB();
alignMTB->process(images, images);
Python
# Align input images
alignMTB = cv2.createAlignMTB()
alignMTB.process(images, images)
第三步:恢复相机响应函数
典型相机的响应对场景亮度不是线性的。这意味着什么?假设,两个物体被相机拍摄,其中一个在现实世界中的亮度是另一个的两倍。当您测量照片中两个物体的像素强度时,较亮物体的像素值不会是较暗物体的两倍!如果不估计相机响应函数 (CRF),我们将无法将图像合并为一张 HDR 图像。
将多张曝光图像合并为 HDR 图像是什么意思?
只考虑图像某个位置 (x,y) 的一个像素。如果 CRF 是线性的,则像素值将与曝光时间成正比,除非像素在特定图像中太暗(即接近 0)或太亮(即接近 255)。我们可以过滤掉这些坏像素(太暗或太亮),并通过将像素值除以曝光时间
// Obtain Camera Response Function (CRF)
Mat responseDebevec;
Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
calibrateDebevec->process(images, responseDebevec, times);
来估计像素处的亮度,然后在像素不错(太暗或太暗)的所有图像中平均该亮度值太亮了 )。我们可以对所有像素执行此操作并获得单个图像,其中所有像素都是通过平均“好”像素获得的。
但是 CRF 不是线性的,我们需要先使图像强度线性化,然后才能通过首先估计 CRF 来合并/平均它们。
好消息是,如果我们知道每张图像的曝光时间,就可以从图像中估计 CRF。与计算机视觉中的许多问题一样,寻找 CRF 的问题被设置为一个优化问题,其目标是最小化由数据项和平滑项组成的目标函数。这些问题通常简化为线性最小二乘问题,使用奇异值分解 (SVD) 来解决,SVD 是所有线性代数包的一部分。CRF 恢复算法的详细信息在题为Recovering High Dynamic Range Radiance Maps from Photos的论文中。
只需使用 OpenCV 中的两行代码,使用CalibrateDebevec或即可找到 CRF CalibrateRobertson。在本教程中,我们将使用CalibrateDebevec。
C++
// Obtain Camera Response Function (CRF)
Mat responseDebevec;
Ptr<CalibrateDebevec> calibrateDebevec = createCalibrateDebevec();
calibrateDebevec->process(images, responseDebevec, times);
Python
# Obtain Camera Response Function (CRF)
calibrateDebevec = cv2.createCalibrateDebevec()
responseDebevec = calibrateDebevec.process(images, times)
下图显示了使用红色、绿色和蓝色通道的图像恢复的 CRF。
第 4 步:合并图像
估计 CRF 后,我们可以使用MergeDebevec
. C++ 和 Python 代码如下所示。
C++
// Merge images into an HDR linear image
Mat hdrDebevec;
Ptr<MergeDebevec> mergeDebevec = createMergeDebevec();
mergeDebevec->process(images, hdrDebevec, times, responseDebevec);
// Save HDR image.
imwrite("hdrDebevec.hdr", hdrDebevec);
Python
# Merge images into an HDR linear image
mergeDebevec = cv2.createMergeDebevec()
hdrDebevec = mergeDebevec.process(images, times, responseDebevec)
# Save HDR image.
cv2.imwrite("hdrDebevec.hdr", hdrDebevec)
上面保存的 HDR 图像可以在 Photoshop 中加载并进行色调映射。一个例子如下所示。
第 5 步:色调映射
将高动态范围 (HDR) 图像转换为每通道 8 位图像同时保留尽可能多的细节的过程称为色调映射。
现在我们已将曝光图像合并为一张 HDR 图像。你能猜出这张图片的最小和最大像素值吗?对于漆黑的情况,最小值显然是 0。理论最大值是多少?无穷!在实践中,不同情况下的最大值是不同的。如果场景包含非常亮的光源,我们将看到一个非常大的最大值。
尽管我们已经使用多张图像恢复了相对亮度信息,但我们现在面临着将这些信息保存为 24 位图像以供显示的挑战。
有几种色调映射算法。OpenCV 实现了其中的四个。要记住的是,没有正确的方法来进行色调映射。通常,我们希望在色调映射图像中看到比在任何一张曝光图像中更多的细节。有时色调映射的目标是产生逼真的图像,而通常目标是产生超现实的图像。OpenCV 中实现的算法倾向于产生真实的结果,因此不太引人注目。
让我们看看各种选项。下面列出了不同色调映射算法的一些常用参数。
1、gamma:此参数通过应用 gamma 校正来压缩动态范围。当 gamma 等于 1 时,不应用校正。小于 1 的 gamma 会使图像变暗,而大于 1 的 gamma 会使图像变亮。
2、饱和度:此参数用于增加或减少饱和度。饱和度高时,颜色更丰富、更强烈。饱和度值更接近于零,使颜色逐渐消失为灰度。
3、contrast:控制输出图像的对比度(即 log (maxPixelValue/minPixelValue) )。
让我们探索 OpenCV 中可用的四种色调映射算法。
Drago Tonemap
Drago Tonemap 的参数如下所示
createTonemapDrago
(
float gamma = 1.0f,
float saturation = 1.0f,
float bias = 0.85f
)
这里,bias是偏置函数在 [0, 1] 范围内的值。0.7 到 0.9 之间的值通常给出最好的结果。默认值为 0.85。
C++ 和 Python 代码如下所示。参数是通过反复试验获得的。最终输出乘以 3 只是因为它给出了最令人满意的结果。
C++
// Tonemap using Drago's method to obtain 24-bit color image
Mat ldrDrago;
Ptr<TonemapDrago> tonemapDrago = createTonemapDrago(1.0, 0.7);
tonemapDrago->process(hdrDebevec, ldrDrago);
ldrDrago = 3 * ldrDrago;
imwrite("ldr-Drago.jpg", ldrDrago * 255);
Python
# Tonemap using Drago's method to obtain 24-bit color image
tonemapDrago = cv2.createTonemapDrago(1.0, 0.7)
ldrDrago = tonemapDrago.process(hdrDebevec)
ldrDrago = 3 * ldrDrago
cv2.imwrite("ldr-Drago.jpg", ldrDrago * 255)
结果
Durand Tonemap
Durand Tonemap 的参数如下所示。
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++
// Tonemap using Durand's method obtain 24-bit color image
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
# Tonemap using Durand's method obtain 24-bit color image
tonemapDurand = cv2.createTonemapDurand(1.5,4,1.0,1,1)
ldrDurand = tonemapDurand.process(hdrDebevec)
ldrDurand = 3 * ldrDurand
cv2.imwrite("ldr-Durand.jpg", ldrDurand * 255)
结果
莱因哈德色调图
createTonemapReinhard
(
float gamma = 1.0f,
float intensity = 0.0f,
float light_adapt = 1.0f,
float color_adapt = 0.0f
)
参数强度应在 [-8, 8] 范围内。更大的强度值会产生更亮的结果。light_adapt控制光照适应,在 [0, 1] 范围内。值 1 表示仅基于像素值的自适应,值 0 表示全局自适应。中间值可用于两者的加权组合。参数color_adapt控制色度适应,在 [0, 1] 范围内。如果该值设置为 1,则通道被独立处理,如果该值设置为 0,则每个通道的适应级别相同。中间值可用于两者的加权组合。
C++
// Tonemap using Reinhard's method to obtain 24-bit color image
Mat ldrReinhard;
Ptr<TonemapReinhard> tonemapReinhard = createTonemapReinhard(1.5, 0,0,0);
tonemapReinhard->process(hdrDebevec, ldrReinhard);
imwrite("ldr-Reinhard.jpg", ldrReinhard * 255);
Python
# Tonemap using Reinhard's method to obtain 24-bit color image
tonemapReinhard = cv2.createTonemapReinhard(1.5, 0,0,0)
ldrReinhard = tonemapReinhard.process(hdrDebevec)
cv2.imwrite("ldr-Reinhard.jpg", ldrReinhard * 255)
结果
曼蒂克色调图
createTonemapMantiuk
(
float gamma = 1.0f,
float scale = 0.7f,
float saturation = 1.0f
)
参数scale是对比度比例因子。从 0.6 到 0.9 的值产生最佳结果。
C++
// Tonemap using Mantiuk's method to obtain 24-bit color image
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
# Tonemap using Mantiuk's method to obtain 24-bit color image
tonemapMantiuk = cv2.createTonemapMantiuk(2.2,0.85, 1.2)
ldrMantiuk = tonemapMantiuk.process(hdrDebevec)
ldrMantiuk = 3 * ldrMantiuk
cv2.imwrite("ldr-Mantiuk.jpg", ldrMantiuk * 255)
结果
三、图片来源
来源这篇文章中使用的四张曝光图像是在CC BY-SA 3.0下获得许可的,并且是从Wikipedia 的 HDR 页面下载的。他们是由凯文·麦考伊拍摄的。
边栏推荐
- 九章云极DataCanvas公司获评36氪「最受投资人关注的硬核科技企业」
- Simulate the implementation of string class
- Some important knowledge of MySQL
- pom. Brief introduction of XML configuration file label function
- CSDN语法说明
- 模拟实现string类
- equals 方法
- 整型int的拼接和拆分
- 国家网信办公布《数据出境安全评估办法》:累计向境外提供10万人信息需申报
- R language uses ggplot2 function to visualize the histogram distribution of counting target variables that need to build Poisson regression model, and analyzes the feasibility of building Poisson regr
猜你喜欢
Welcome to the markdown editor
The project manager's "eight interview questions" is equal to a meeting
Navicat连接2002 - Can‘t connect to local MySQL server through socket ‘/var/lib/mysql/mysql.sock‘解决
9 原子操作类之18罗汉增强
开源OA开发平台:合同管理使用手册
开源重器!九章云极DataCanvas公司YLearn因果学习开源项目即将发布!
CSDN语法说明
mock.js从对象数组中任选数据返回一个数组
Openeuler prize catching activities, to participate in?
PMP對工作有益嗎?怎麼選擇靠譜平臺讓備考更省心省力!!!
随机推荐
一锅乱炖,npm、yarn cnpm常用命令合集
ASP. Net kindergarten chain management system source code
数据孤岛是企业数字化转型遇到的第一道险关
国家网信办公布《数据出境安全评估办法》:累计向境外提供10万人信息需申报
JVM GC垃圾回收简述
R语言ggplot2可视化:使用ggpubr包的ggqqplot函数可视化QQ图(Quantile-Quantile plot)
A pot of stew, a collection of common commands of NPM and yarn cnpm
Welcome to the markdown editor
JVM 类加载机制
vulnhub之school 1
力扣 2319. 判断矩阵是否是一个 X 矩阵
Semantic SLAM源码解析
Ucloud is a basic cloud computing service provider
R language uses ggplot2 function to visualize the histogram distribution of counting target variables that need to build Poisson regression model, and analyzes the feasibility of building Poisson regr
gorilla官方:golang开websocket client的示例代码
微信公众号OAuth2.0授权登录并显示用户信息
R language ggplot2 visualization: use the ggqqplot function of ggpubr package to visualize the QQ graph (Quantitative quantitative plot)
ASP.NET幼儿园连锁管理系统源码
mock. JS returns an array from the optional data in the object array
Some arrangements about oneself