下一个教程: 高级别的Stitching API(Stitcher类)
Original author | Fedor Morozov |
---|---|
Compatibility | OpenCV >= 3.0 |
简介
今天,大多数数字图像和成像设备使用每通道8比特,从而将设备的动态范围限制在两个数量级(实际上是256级),而人眼可以适应10个数量级的照明条件变化。当我们拍摄现实世界场景的照片时,明亮的区域可能曝光过度,而黑暗的区域可能曝光不足,所以我们不能用一次曝光来捕捉所有细节。HDR成像的工作原理是每个通道使用8位以上的图像(通常是32位浮动值),允许更广泛的动态范围。
有不同的方法来获得HDR图像,但最常见的方法是使用不同曝光值拍摄的场景的照片。为了结合这些曝光,了解你的相机的响应函数是很有用的,有一些算法可以估计它。在HDR图像被混合后,它必须被转换回8位,以便在通常的显示器上观看。这个过程被称为色调映射。当场景中的物体或相机在拍摄之间移动时,会产生额外的复杂性,因为不同曝光的图像应该被登记和对齐。
在本教程中,我们展示了如何从曝光序列中生成和显示HDR图像。在我们的案例中,图像已经对齐,没有移动的物体。我们还展示了另一种方法,称为曝光融合,产生低动态范围图像。HDR管道的每一步都可以用不同的算法来实现,所以请看参考手册来了解它们。
曝光序列
源代码
本教程的代码如下所示。你也可以从这里下载:
- C++
#include "opencv2/photo.hpp"
#include "opencv2/imgcodecs.hpp"
#include "opencv2/highgui.hpp"
#include <vector>
#include <iostream>
#include <fstream>
using namespace cv;
using namespace std;
void loadExposureSeq(String, vector<Mat>&, vector<float>&);
int main(int argc, char**argv)
{
CommandLineParser parser( argc, argv, "{@input | | Input directory that contains images and exposure times. }" );
vector<Mat> images;
vector<float> times;
loadExposureSeq(parser.get<String>( "@input" ), images, times);
Mat response;
Ptr<CalibrateDebevec> calibrate = createCalibrateDebevec();
calibrate->process(images, response, times);
Mat hdr;
Ptr<MergeDebevec> merge_debevec = createMergeDebevec();
merge_debevec->process(images, hdr, times, response);
Mat ldr;
Ptr<Tonemap> tonemap = createTonemap(2.2f);
tonemap->process(hdr, ldr);
Mat fusion;
Ptr<MergeMertens> merge_mertens = createMergeMertens();
merge_mertens->process(images, fusion);
imwrite("fusion.png", fusion * 255);
imwrite("ldr.png", ldr * 255);
imwrite("hdr.hdr", hdr);
return 0;
}
void loadExposureSeq(String path, vector<Mat>& images, vector<float>& times)
{
path = path + "/";
ifstream list_file((path + "list.txt").c_str());
string name;
float val;
while(list_file >> name >> val) {
Mat img = imread(path + name);
images.push_back(img);
times.push_back(1 / val);
}
list_file.close();
}
- Java
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import org.opencv.core.Core;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Scalar;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.photo.CalibrateDebevec;
import org.opencv.photo.MergeDebevec;
import org.opencv.photo.MergeMertens;
import org.opencv.photo.Photo;
import org.opencv.photo.Tonemap;
class HDRImaging {
public void loadExposureSeq(String path, List<Mat> images, List<Float> times) {
path += "/";
List<String> lines;
try {
lines = Files.readAllLines(Paths.get(path + "list.txt"));
for (String line : lines) {
String[] splitStr = line.split("\\s+");
if (splitStr.length == 2) {
String name = splitStr[0];
Mat img = Imgcodecs.imread(path + name);
images.add(img);
float val = Float.parseFloat(splitStr[1]);
times.add(1/ val);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void run(String[] args) {
String path = args.length > 0 ? args[0] : "";
if (path.isEmpty()) {
System.out.println("Path is empty. Use the directory that contains images and exposure times.");
System.exit(0);
}
List<Mat> images = new ArrayList<>();
List<Float> times = new ArrayList<>();
loadExposureSeq(path, images, times);
Mat response = new Mat();
CalibrateDebevec calibrate = Photo.createCalibrateDebevec();
Mat matTimes = new Mat(times.size(), 1, CvType.CV_32F);
float[] arrayTimes = new float[(int) (matTimes.total()*matTimes.channels())];
for (int i = 0; i < times.size(); i++) {
arrayTimes[i] = times.get(i);
}
matTimes.put(0, 0, arrayTimes);
calibrate.process(images, response, matTimes);
Mat hdr = new Mat();
MergeDebevec mergeDebevec = Photo.createMergeDebevec();
mergeDebevec.process(images, hdr, matTimes);
Mat ldr = new Mat();
Tonemap tonemap = Photo.createTonemap(2.2f);
tonemap.process(hdr, ldr);
Mat fusion = new Mat();
MergeMertens mergeMertens = Photo.createMergeMertens();
mergeMertens.process(images, fusion);
Core.multiply(fusion, new Scalar(255,255,255), fusion);
Core.multiply(ldr, new Scalar(255,255,255), ldr);
Imgcodecs.imwrite("fusion.png", fusion);
Imgcodecs.imwrite("ldr.png", ldr);
Imgcodecs.imwrite("hdr.hdr", hdr);
System.exit(0);
}
}
public class HDRImagingDemo {
public static void main(String[] args) {
// Load the native OpenCV library
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
new HDRImaging().run(args);
}
}
- Python
from __future__ import print_function
from __future__ import division
import cv2 as cv
import numpy as np
import argparse
import os
def loadExposureSeq(path):
images = []
times = []
with open(os.path.join(path, 'list.txt')) as f:
content = f.readlines()
for line in content:
tokens = line.split()
images.append(cv.imread(os.path.join(path, tokens[0])))
times.append(1 / float(tokens[1]))
return images, np.asarray(times, dtype=np.float32)
parser = argparse.ArgumentParser(description='Code for High Dynamic Range Imaging tutorial.')
parser.add_argument('--input', type=str, help='Path to the directory that contains images and exposure times.')
args = parser.parse_args()
if not args.input:
parser.print_help()
exit(0)
images, times = loadExposureSeq(args.input)
calibrate = cv.createCalibrateDebevec()
response = calibrate.process(images, times)
merge_debevec = cv.createMergeDebevec()
hdr = merge_debevec.process(images, times, response)
tonemap = cv.createTonemap(2.2)
ldr = tonemap.process(hdr)
merge_mertens = cv.createMergeMertens()
fusion = merge_mertens.process(images)
cv.imwrite('fusion.png', fusion * 255)
cv.imwrite('ldr.png', ldr * 255)
cv.imwrite('hdr.hdr', hdr)
图像样本
包含图像、曝光时间和list.txt
文件的数据目录可以从这里下载。
解释
- 加载图像和曝光时间
- C++
vector<Mat> images;
vector<float> times;
loadExposureSeq(parser.get<String>( "@input" ), images, times);
- Java
List<Mat> images = new ArrayList<>();
List<Float> times = new ArrayList<>();
loadExposureSeq(path, images, times);
- Python
images, times = loadExposureSeq(args.input)
首先,我们从用户定义的文件夹中加载输入图像和曝光时间。该文件夹应包含图像和list.txt–包含文件名和反转曝光时间的文件。
对于我们的图像序列,列表如下:
memorial00.png 0.03125
memorial01.png 0.0625
...
memorial15.png 1024
- 估算摄像机的响应
- C++
Mat response;
Ptr<CalibrateDebevec> calibrate = createCalibrateDebevec();
calibrate->process(images, response, times);
- Java
Mat response = new Mat();
CalibrateDebevec calibrate = Photo.createCalibrateDebevec();
Mat matTimes = new Mat(times.size(), 1, CvType.CV_32F);
float[] arrayTimes = new float[(int) (matTimes.total()*matTimes.channels())];
for (int i = 0; i < times.size(); i++) {
arrayTimes[i] = times.get(i);
}
matTimes.put(0, 0, arrayTimes);
calibrate.process(images, response, matTimes);
- Python
calibrate = cv.createCalibrateDebevec()
response = calibrate.process(images, times)
对于很多HDR构建算法来说,知道相机响应函数(CRF)是必要的。我们使用其中一种校准算法来估计所有256个像素值的逆CRF。
- 制作HDR图像
- C++
Mat hdr;
Ptr<MergeDebevec> merge_debevec = createMergeDebevec();
merge_debevec->process(images, hdr, times, response);
- Java
Mat hdr = new Mat();
MergeDebevec mergeDebevec = Photo.createMergeDebevec();
mergeDebevec.process(images, hdr, matTimes);
- Python
merge_debevec = cv.createMergeDebevec()
hdr = merge_debevec.process(images, times, response)
我们使用Debevec的加权方案来构建HDR图像,使用前一项计算的响应。
- 音频地图HDR图像
- C++
Mat ldr;
Ptr<Tonemap> tonemap = createTonemap(2.2f);
tonemap->process(hdr, ldr);
- Java
Mat ldr = new Mat();
Tonemap tonemap = Photo.createTonemap(2.2f);
tonemap.process(hdr, ldr);
- Python
tonemap = cv.createTonemap(2.2)
ldr = tonemap.process(hdr)
由于我们想在普通的LDR显示器上看到我们的结果,我们必须将我们的HDR图像映射到8位范围,保留大部分细节。这也是色调映射方法的主要目标。我们使用带有双边过滤的色调映射器,并设置2.2作为伽玛校正值。
- 进行曝光融合
- C++
Mat fusion;
Ptr<MergeMertens> merge_mertens = createMergeMertens();
merge_mertens->process(images, fusion);
- Java
Mat fusion = new Mat();
MergeMertens mergeMertens = Photo.createMergeMertens();
mergeMertens.process(images, fusion);
- Python
merge_mertens = cv.createMergeMertens()
fusion = merge_mertens.process(images)
在我们不需要HDR图像的情况下,有一种替代方法来合并我们的曝光。这个过程被称为曝光融合,产生不需要伽马校正的LDR图像。它也不使用照片的曝光值。
-
在我们不需要HDR图像的情况下,有一种替代方法来合并我们的曝光。这个过程被称为曝光融合,产生不需要伽马校正的LDR图像。它也不使用照片的曝光值。
-
写下结果
-
C++
imwrite("fusion.png", fusion * 255);
imwrite("ldr.png", ldr * 255);
imwrite("hdr.hdr", hdr);
- Java
Core.multiply(fusion, new Scalar(255,255,255), fusion);
Core.multiply(ldr, new Scalar(255,255,255), ldr);
Imgcodecs.imwrite("fusion.png", fusion);
Imgcodecs.imwrite("ldr.png", ldr);
Imgcodecs.imwrite("hdr.hdr", hdr);
- Python
cv.imwrite('fusion.png', fusion * 255)
cv.imwrite('ldr.png', ldr * 255)
cv.imwrite('hdr.hdr', hdr)
现在是时候看一下结果了。请注意,HDR图像不能以常见的图像格式存储,所以我们把它保存为Radiance图像(.hdr)。另外,所有的HDR成像函数返回的结果都在[0, 1]范围内,所以我们应该把结果乘以255。
你可以尝试其他的色调图算法:cv::TonemapDrago, cv::TonemapMantiuk和cv::TonemapReinhard 你也可以为你自己的照片调整HDR校准和色调图方法中的参数。
结果
色调映射的图像
曝光融合
其他资源
- Paul E Debevec和Jitendra Malik. 从照片中恢复高动态范围辐射图。在ACM SIGGRAPH 2008班,第31页。ACM,2008年。(Paul E Debevec and Jitendra Malik. Recovering high dynamic range radiance maps from photographs. In ACM SIGGRAPH 2008 classes, page 31. ACM, 2008)[57]
- Mark A Robertson, Sean Borman, and Robert L Stevenson. 通过多次曝光改善动态范围。In Image Processing, 1999. ICIP 99. Proceedings. 1999年国际会议,第3卷,第159-163页。IEEE,1999年。(Mark A Robertson, Sean Borman, and Robert L Stevenson. Dynamic range improvement through multiple exposures. In Image Processing, 1999. ICIP 99. Proceedings. 1999 International Conference on, volume 3, pages 159–163. IEEE, 1999.)[209]
- Tom Mertens, Jan Kautz, and Frank Van Reeth. 曝光融合. In Computer Graphics and Applications, 2007. PG’07. 第15届太平洋会议,第382-390页。IEEE, 2007.(Tom Mertens, Jan Kautz, and Frank Van Reeth. Exposure fusion. In Computer Graphics and Applications, 2007. PG’07. 15th Pacific Conference on, pages 382–390. IEEE, 2007. ) [172]
- 维基百科-HDR
- 从照片中恢复高动态范围辐射图(网页)