OpenCV4学习笔记(59)——高动态范围(HDR)成像

今天要整理记录的笔记是与高动态范围HDR成像相关的内容,主要参考学习的来源为OpenCV官方文档。

首先需要了解什么是高动态范围HDR,由维基百科定义可知:

高动态范围成像(英语:High Dynamic Range Imaging,简称HDRI或HDR),在计算机图形学与电影摄影术中,是用来实现比普通数位图像技术更大曝光动态范围(即更大的明暗差别)的一组技术。高动态范围成像的目的就是要正确地表示真实世界中从太阳光直射到最暗的阴影这样大的范围亮度。

也就是说,HDR图像相比起我们平常所见的图像来说,其像素取值范围更大、深度更深,一般的图像可能是8位的无符号字节类型,而HDR图像则一般为32位浮点类型,所以HDR图像所能存储的图像信息相比起一般图像要多得多,最直观的视觉感受就是:HDR图像的明亮处和灰暗处都能够呈现出比较好的细节,不会出现严重的过曝和欠曝问题。

但也正因为HDR图像存储的信息量之大、深度之深,以至于我们不能直接进行显示,会出现一片白茫茫的情况。而需要先进行色调映射后才可以正常显示。

下面就跟随着官方文档的步骤,来学习实现HDR图像的合成与显示。在OpenCV中,提供了三种合成HDR图像的方式,分别是Debevec方法、Robertson方法、Mertens方法,由于这三种方式使用起来具有一定的相似性,所以下面并行进行演示。

  1. 加载曝光图像序列
    首先,不管是哪一种方法,我们都需要加载一系列不同曝光时间(以s为单位)的图像,如果把这一系列图像单独存储在一个文件夹中,可以用以下方式读取并组织成一个曝光图像序列。
	//加载曝光图像序列
	vector<string>imgPath;
	glob("D:\\opencv_c++\\opencv_tutorial\\data\\images\\exposures\\", imgPath);
	vector<Mat> exposure_images;
	for (int i = 0;i < imgPath.size();i++)
	{
		Mat exposure_image = imread(imgPath[i]);
		exposure_images.push_back(exposure_image);
	}
  1. 加载曝光时间序列
    对于Debevec方法和Robertson方法来说,在进行HDR合成时需要同时传入曝光图像序列和曝光时间序列,而对于Mertens方法来说只需要传入曝光图像序列即可。这里还是需要进行加载曝光时间序列这一步骤。
    当把不同曝光时间用txt文件存储时,可以用以下方式来加载曝光时间序列。
	//加载曝光时间序列
	String exposureTimesPath = "D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\exposuresTime.txt";
	ifstream fp(exposureTimesPath);
	if (!fp.is_open())
	{
		cout << "can't open file" << endl;
		exit(-1);
	}
	vector<float> exposureTimes;
	while (!fp.eof())
	{
		string time;
		getline(fp, time);
		float t = float(atof(time.c_str()));
		exposureTimes.push_back(t);
	}
  1. 进行HDR合成
    (1)Robertson方法与Debevec方法
    使用Robertson方法和Debevec方法进行HDR图像合成时,需要将多张不同曝光时间的照片及其对应的曝光值进行加权平均,得到float32类型的高动态范围HDR图像,并且可以加入预先计算得到的相机响应函数(CRF)进行非线性合成。如果没有加入相机响应函数(CRF)这个参数,则会通过传入的不同曝光图像序列和不同曝光时间序列来进行计算得到一个线性CRF。
    不过,在OpenCV中对于相机响应函数这个参数的设置应该有些问题。如果不传入CRF这个参数,那么合成的HDR图像效果比较好,而一旦加入CRF参数,那么得到的HDR图像将会呈现非常差的效果。也就是说,当在OpenCV中合成HDR图像时,使用线性CRF得到的HDR图像,其效果相比起预先计算CRF后传入参数来得到的HDR图像要好得多。
    这个问题让我很疑惑,明明加入预先计算的相机响应函数后,应该得到更好的HDR效果才对,但是实际上恰恰相反,而且在网上找了很久也没有相关的解答。最终只发现在Stack Overflow上有一位网友也提出了相同的问题,并在讨论区有网友认为是OpenCV的合成HDR功能的API中已经加入了伽马矫正的内容,以至于再次进行非线性矫正的情况下反而会导致更差的效果。
    总而言之 ,从理论上来说,我们需要预先计算CRF来参与HDR图像合成才能得到好的效果,但是在OpenCV中并不一定需要这一步,至少在我以官网中的图像做实验时,不加入CRF参数能得到更好的效果。
    而且虽然官方文档中是有传入CRF参数的,但是我以相同的参数对相同的图像做处理时却得不到和官方文档中相同的HDR图像,反而是不加入CRF参数时效果更好。
    当我们合成得到HDR图像后,由于是在普通的低动态范围LDR显示器上进行显示,因此我们必须将HDR图像映射到保留大多数细节的8位范围的低动态范围LDR图像。这一步也称为色调映射,目的就是为了将我们得到的HDR图像正常显示出来,不过从高动态范围到低动态范围,肯定会牺牲一些图像信息。
    下面是Robertson方法与Debevec方法的代码演示:
void Debevec(vector<Mat>exposureImages, vector<float>exposureTimes)
{
	//auto Debevec_response = createCalibrateDebevec();
	//Mat response;
	//Debevec_response->process(exposureImages, response, exposureTimes);

	auto  merge_Debevec = createMergeDebevec();
	Mat hdr;
	merge_Debevec->process(exposureImages, hdr, exposureTimes);
	//由于是在普通的LDR显示器上进行显示,因此我们必须将HDR图像映射到保留大多数细节的8位范围的低动态范围LDR图像。
	//这是进行色调映射的主要目标,伽玛校正值设置为2.2f适用于大多数情况
	Mat ldr;
	auto tonemap = createTonemap(2.2f);
	tonemap->process(hdr, ldr);
	//HDR图像进行色调映射后得到的LDR图像取值范围在 [ 0 , 1 ] ,所以乘255将范围拉伸到 [ 0 , 255 ] 
	ldr = ldr * 255;
	//将float32类型转化为uchar8类型
	ldr.convertTo(ldr, CV_8UC3);
	imshow("Debevec_HDR", hdr);
	imshow("Debevec_LDR", ldr);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Debevec_LDR.png", ldr);
}
void Robertson(vector<Mat>exposureImages, vector<float>exposureTimes)
{
	//auto Robertson_response = createCalibrateRobertson();
	//Mat response;
	//Robertson_response->process(exposureImages, response, exposureTimes);

	auto  merge_Robertson = createMergeRobertson();
	Mat hdr;
	merge_Robertson->process(exposureImages, hdr, exposureTimes);
	//由于是在普通的LDR显示器上进行显示,因此我们必须将HDR图像映射到保留大多数细节的8位范围的低动态范围LDR图像。
	//这是进行色调映射的主要目标,伽玛校正值设置为2.2f适用于大多数情况
	Mat ldr;
	auto tonemap = createTonemap(2.2f);
	tonemap->process(hdr, ldr);
	//HDR图像进行色调映射后得到的LDR图像取值范围在 [ 0 , 1 ] ,所以乘255将范围拉伸到 [ 0 , 255 ] 
	ldr = ldr * 255;
	ldr.convertTo(ldr, CV_8UC3);
	imshow("Robertson_HDR", hdr);
	imshow("Robertson_LDR", ldr);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Robertson_LDR.png", ldr);

}

(2)Mertens方法
对于Mertens方法来说,只需要传入不同曝光时间的图像序列就可以了,该方法使用对比度、饱和度和曝光度的好坏来对像素加权,而不是使用拉普拉斯金字塔来对图像进行加权。得到的图像权重被构造为对比度、饱和度和曝光度良好的加权平均值。
由于Mertens方法是进行不同曝光时间图像的融合,并且直接生成由HDR图像转化来的低动态范围图像,所以生成的图像不需要进行色调映射。
生成的图像取值范围在 [ 0 , 1 ] 之间,可以通过乘以255将取值范围拉伸到 [ 0 , 255 ],并且转换为8位图像进行显示。在官方文档中还建议应用伽马校正或线性色调映射来提升显示效果,OpenCV中提供了三种色调映射方法,这里使用的是createTonemap(1.0f),其中参数的默认值是1.0f,不过取2.2f能适用于大多数场景。
当我们不清楚图像的曝光时间,或者不需要得到HDR图像时,可以直接使用Mertens方法来进行曝光融合,得到的低动态范围图像也是由HDR图像转换而来,同样在明亮处和灰暗处都有更好的细节体现。
下面是Mertens方法的代码演示:

void Mertens(vector<Mat>exposureImages)
{
	auto merge_mertens = createMergeMertens();
	Mat fusion;
	merge_mertens->process(exposureImages, fusion);
	//进行伽马矫正,需根据实际图像调节参数,2.2f可满足大多数显示情况
	/*auto tonemap = createTonemap(2.2f);
	tonemap->process(fusion, fusion);*/
	fusion = fusion * 255;
	fusion.convertTo(fusion, CV_8UC3);
	imshow("Mertens", fusion);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Mertens.png", fusion);
}

到这里就分别以不同方法给出了OpenCV中HDR图像的合成步骤,下面是完整的代码演示:

#include<opencv2/opencv.hpp>
#include<iostream>
#include<vector>
#include<fstream>
#include<stdlib.h>
using namespace std;
using namespace cv;


void Debevec(vector<Mat>exposureImages, vector<float>exposureTimes);
void Robertson(vector<Mat>exposureImages, vector<float>exposureTimes);
void Mertens(vector<Mat>exposureImages);
int main()
{
	//加载曝光图像序列
	vector<string>imgPath;
	glob("D:\\opencv_c++\\opencv_tutorial\\data\\images\\exposures\\", imgPath);
	vector<Mat> exposure_images;
	for (int i = 0;i < imgPath.size();i++)
	{
		Mat exposure_image = imread(imgPath[i]);
		exposure_images.push_back(exposure_image);
	}
	//加载曝光时间序列
	String exposureTimesPath = "D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\exposuresTime.txt";
	ifstream fp(exposureTimesPath);
	if (!fp.is_open())
	{
		cout << "can't open file" << endl;
		exit(-1);
	}
	vector<float> exposureTimes;
	while (!fp.eof())
	{
		string time;
		getline(fp, time);
		float t = float(atof(time.c_str()));
		exposureTimes.push_back(t);
	}
	//进行HDR合成
	Debevec(exposure_images, exposureTimes);
	Robertson(exposure_images, exposureTimes);
	Mertens(exposure_images);

	waitKey(0);
	return 0;
}


//使用对比度、饱和度和曝光度好坏来对像素加权,而不是使用拉普拉斯金字塔来对图像进行加权。得到的图像权重被构造为对比度、饱和度和曝光度良好度量的加权平均值。
//生成的图像不需要进行色调映射,并且可以通过乘以255转换为8位图像,但是建议应用伽马校正或线性色调映射。
//不需要对应曝光时间, 可以在不需要HDR图像时使用,可直接得到低动态范围图像
void Mertens(vector<Mat>exposureImages)
{
	auto merge_mertens = createMergeMertens();
	Mat fusion;
	merge_mertens->process(exposureImages, fusion);
	//进行伽马矫正,需根据实际图像调节参数,2.2f可满足大多数显示情况
	/*auto tonemap = createTonemap(2.2f);
	tonemap->process(fusion, fusion);*/
	fusion = fusion * 255;
	fusion.convertTo(fusion, CV_8UC3);
	imshow("Mertens", fusion);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Mertens.png", fusion);
}


//使用Debevec方法进行HDR合成,将多张曝光时间不同的照片及其对应的曝光值进行加权平均,得到float32类型的高动态范围HDR图像
//需要传入曝光图像序列和对应的曝光时间
void Debevec(vector<Mat>exposureImages, vector<float>exposureTimes)
{
	//auto Debevec_response = createCalibrateDebevec();
	//Mat response;
	//Debevec_response->process(exposureImages, response, exposureTimes);

	auto  merge_Debevec = createMergeDebevec();
	Mat hdr;
	merge_Debevec->process(exposureImages, hdr, exposureTimes);
	//由于是在普通的LDR显示器上进行显示,因此我们必须将HDR图像映射到保留大多数细节的8位范围的低动态范围LDR图像。
	//这是进行色调映射的主要目标,伽玛校正值设置为2.2f适用于大多数情况
	Mat ldr;
	auto tonemap = createTonemap(2.2f);
	tonemap->process(hdr, ldr);
	//HDR图像进行色调映射后得到的LDR图像取值范围在 [ 0 , 1 ] ,所以乘255将范围拉伸到 [ 0 , 255 ] 
	ldr = ldr * 255;
	//将float32类型转化为uchar8类型
	ldr.convertTo(ldr, CV_8UC3);
	imshow("Debevec_HDR", hdr);
	imshow("Debevec_LDR", ldr);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Debevec_LDR.png", ldr);
}


//使用Robertson方法进行HDR图像合成,将多张曝光时间不同的照片及其对应的曝光值进行加权平均,得到float32类型的高动态范围HDR图像
//需要传入曝光图像序列和对应的曝光时间
void Robertson(vector<Mat>exposureImages, vector<float>exposureTimes)
{
	//auto Robertson_response = createCalibrateRobertson();
	//Mat response;
	//Robertson_response->process(exposureImages, response, exposureTimes);

	auto  merge_Robertson = createMergeRobertson();
	Mat hdr;
	merge_Robertson->process(exposureImages, hdr, exposureTimes);
	//由于是在普通的LDR显示器上进行显示,因此我们必须将HDR图像映射到保留大多数细节的8位范围的低动态范围LDR图像。
	//这是进行色调映射的主要目标,伽玛校正值设置为2.2f适用于大多数情况
	Mat ldr;
	auto tonemap = createTonemap(2.2f);
	tonemap->process(hdr, ldr);
	//HDR图像进行色调映射后得到的LDR图像取值范围在 [ 0 , 1 ] ,所以乘255将范围拉伸到 [ 0 , 255 ] 
	ldr = ldr * 255;
	ldr.convertTo(ldr, CV_8UC3);
	imshow("Robertson_HDR", hdr);
	imshow("Robertson_LDR", ldr);
	imwrite("D:\\opencv_c++\\Learning-OpenCV\\高动态(HDR)图像\\Robertson_LDR.png", ldr);

}

分别看一下不同方法合成的HDR图像的效果如何,首先看一下所使用的曝光图像序列:
在这里插入图片描述
我们使用上述16张不同曝光时间的图像进行合成HDR图像,三种方法效果图如下:
Debevec_LDR.png
Robertson_LDR.png
Mertens.png
从上到下分别是Debevec方法、Robertson方法、Mertens方法得到的HDR图像显示效果,总的来说在亮处和暗处都呈现出更多的细节,但是Debevec方法和Robertson方法在色调映射这一步骤上仍然需要更多的调试,可以选取不同参数取值或选取不同的色调映射方法来进行尝试,以获得更好的效果。在这里,我们只是尝试还原了官方文档中的HDR效果,虽然官网中演示代码在合成HDR图像时加入了CRF参数而我们的演示代码中并没有,但是效果还是比较相似的。而一旦加入相机响应函数CRF,就会导致效果变得很差,甚至严重偏色,至于原因我还没找到,希望有了解这个的朋友能指导我一下。

最后,虽然在这组图像中,Mertens方法得到的HDR效果是最好的,但并不意味着对于所有图像都是如此,我们在实际使用时还需要分别测试不同方法对于我们实际图像的呈现效果,并且还需要对色调映射的不同方法、不同参数进行调试。只有经过不断调试、对比,才能得到最符合我们实际情况的HDR成像方法以及参数设置。

PS:本人的注释比较杂,既有自己的心得体会也有网上查阅资料时摘抄下的知识内容,所以如有雷同,纯属我向前辈学习的致敬,如果有前辈觉得我的笔记内容侵犯了您的知识产权,请和我联系,我会将涉及到的博文内容删除,谢谢!

猜你喜欢

转载自blog.csdn.net/weixin_45224869/article/details/105895367