原文见http://blog.csdn.net/zzzGoogle/article/details/1615691
1、HDR简介
HDR的全称是High-Dynamic Range(高动态范围)。在此,我们先解释一下什么是Dynamic Range(动态范围),动态范围是指图像中所包含的从“最亮”至“最暗”的比值,也就是图像从“最亮”到“最暗”之间灰度划分的等级数;动态范围越大,所能表示的层次越丰富,所包含的色彩空间也越广。那高动态范围(HDR)顾名思义就是从“最亮”到“最暗”可以达到非常高的比值。
在日常生活中我们经常遇到这样的情况:突然从黑暗的房间中走到阳光下,眼睛会无法睁开;清晨阳光会穿透窗帘像光柱般照射入房间;反光度较高的物体在强光下会在周围产生光晕。以上这些生活中随处可见的现象在有HDR以前无法在3D世界中呈现!最大的原因就在于我们使用8~16bit的整数数据,使用8~16bit的整数数据是整个图象处理失真的关键点,所以我们对以往的运算方法做了以下二方面的重大改进:
1、使用16bit、32bit的数据来提高像素数据的精度。既然继续使用8bit的数据来记录像素的特征不能满足HDR数据所需要的高精度运算的要求,在这种情况下,我们考虑使用16bit、32bit的数据记录来提高像素数据的精度都是可以的。使用了更多的数据来保存像素特征之后,无论是像素的对比度还是像素可以体现的色彩数目都有了巨大的提高。
2、图象数据采用浮点数据。HDR真正的巨大变革来自于浮点数据的引入。我们可以采用浮点方式来处理和存放亮度数据,抛弃不准确的整数数据;同时计算机在引入浮点数据来存储象素的各个参数并且在运算的全过程都使用浮点数据,这样就可以有效的提高据的精确度。
那么采用HDR后动态范围最大可以有多大呢?我们看如下的公式,就可以知道我们到底使用了HDR后动态值可以有多大,而动态值的大小直接表现了动态范围的大小:Dynamic Range=log10(Max Intensity / Min Intensity)。公式中intensity是指强度,我们对最大亮度除以最低亮度的结果取对数,得到的结果就是动态范围的相对数值。根据公式计算,当我们在亮度通道使用8bit的的情况下,最高亮度255,最低亮度1。那么计算得出的动态范围就是数值约为2.4,加上单位就是24dB。同理可以计算得出16bit 的亮度通道的动态范围是数值约是4.8,是使用8bit亮度通道的一倍。理论上在HDR模式下,动态范围的数值最高可以到达76.8。在NVIDIA所使用的OpenEXR中表现出来的HDR动态范围的数值最大值约有12.0,远远高出单纯使用16bit亮度通道的所带来的亮度体验,这是采用了优秀算法的结果。OpenEXR所能实现的最大动态范围已经超过了人眼的9,带来了更加真实的视觉体验。
2、HDRI文件格式介绍(OpenEXR、Radiance RGBE、Float TIFF)
HDRI(High-Dynamic Range Image)就是记录采用了HDR技术的图象数据文件。常用的HDRI文件有OpenEXR、Radiance RGBE、FloatTIFF三种格式。
2.1 OpenEXR文件格式
OpenEXR是由工业光魔(Industrial Light & Magic)开发的一种HDR标准。OpenEXR文件的扩展名为.exr,常见的OpenEXR文件是FP16(16bit Float Point,也被称为half Float Point)数据图像文件,每个通道的数据类型是FP16,一共四个通道64bpp,每个通道1个bit位用来标志“指数”,5个bit用来存放指数的值,10个bit存放色度坐标(u,v)的尾数,其动态范围从6.14 × 10 ^ -5到6.41 × 10 ^ 4。
在OpenEXR的算法里面共使用16bit来表示光照数据。虽然看起来和使用16bit亮度通道运算位数相同,但是OpenEXR巧妙的采用了1个bit位用来标志“指数”,5个bit用来存放指数的值,10个bit存放色度坐标的尾数。这样就轻易的解决了浮点数值由于位数少而精度不高的问题。大大的拓宽的在FP16下的动态范围。根据实际的计算结果:在正规化的情况下OpenEXR可以提供和人眼基本相同的动态范围,最暗到最亮是0.00006103515625(6.14 × 10 ^ -5)到65504(6.41 × 10 ^ 4),动态范围是9.03;非正规化条件下,OpenEXR可以提供从最暗到最亮的数值从0.000000059604644775390625(5.96 × 10 ^ -8 )到65504(6.41 × 10 ^ 4),化为动态范围表示就是12。
下面是Still写的OpenEXR读写代码,保存的.exr文件采用Zips压缩编码。
bool COpenExr::Load(const char fileName[], int& width, int& height, float** pixels)
{
std::vector<float> vecpixels;
if(!Load(fileName, width, height, vecpixels))
return false;
int num = width * height * 3;
*pixels = new float[num];
if(NULL == *pixels)
return false;
std::vector<float>::pointer ptr = &vecpixels[0];
memcpy(*pixels, ptr, num * 4);
return true;
}
bool COpenExr::Load(const char fileName[], int& width, int& height, std::vector<float> &pixels)
{
Imf::Array<Imf::Rgba> pixelsdata;
bool bLoad = loadImage(fileName, width, height, pixelsdata);
if(!bLoad) return false;
for(int y = 0; y < height; y++)
{
int i = y * width;
for(int x = 0; x < width; x++)
{
int j = i + x;
const Imf::Rgba &rp = pixelsdata[j];
pixels.push_back( float(rp.r));
pixels.push_back( float(rp.g));
pixels.push_back( float(rp.b));
}
}
return true;
}
bool COpenExr::loadImage (const char fileName[], int& width, int& height, Imf::Array<Imf::Rgba>& pixels)
{
Imf::RgbaInputFile in (fileName);
Imath::Box2i dataWindow = in.dataWindow();
int dw, dh, dx, dy;
width = dw = dataWindow.max.x - dataWindow.min.x + 1;
height = dh = dataWindow.max.y - dataWindow.min.y + 1;
dx = dataWindow.min.x;
dy = dataWindow.min.y;
pixels.resizeErase (dw * dh);
in.setFrameBuffer (pixels - dx - dy * dw, 1, dw);
try
{
in.readPixels (dataWindow.min.y, dataWindow.max.y);
}catch (const exception &e)
{
std::cerr << e.what() << std::endl;
return false;
}
return true;
}
bool COpenExr::Save(const char fileName[], int width, int height, const float* pixels)
{
std::vector<float> vecpixels(pixels, pixels + width * height * 3);
return Save(fileName, width, height, vecpixels);
}
bool COpenExr::Save(const char fileName[], int width, int height, const std::vector<float> pixels)
{
Imf::Array<Imf::Rgba> pixelsdata;
pixelsdata.resizeErase(width * height);
for(int y = 0; y < height; y++)
{
int i = y * width;
for(int x = 0; x < width; x++)
{
int j = i + x;
half r = pixels[j * 3 ];
half g = pixels[j * 3 + 1];
half b = pixels[j * 3 + 2];
pixelsdata[j] = Imf::Rgba(r, g, b);
}
}
return SaveImage(fileName, width, height, pixelsdata);
}
bool COpenExr::SaveImage(const char fileName[], int width, int height, const Imf::Array<Imf::Rgba> &pixels)
{
Imf::RgbaOutputFile file (fileName, width, height);
file.setFrameBuffer(pixels, 1, width);
try
{
file.writePixels(height);
}catch(const exception &e)
{
std::cerr<< e.what() <<std::endl;
return false;
}
return true;
}
官方库链接地址:http://www.openexr.com/
2.2 Radiance RGBE文件格式
RGBE文件的扩展名为.hdr,RGBE正式名称为Radiance RGBE格式。这个本来是BR、FR等作为radiance材质的一种格式,也叫做radiance map,后来成为流行的一种HDR格式。所谓E,就是指数。Radiance RGBE文件每个通道为8bit BYTE数据类型,4个通道一共是32 bit。RGBE可以使用RLE压缩编码压缩,也可以不压缩。由文件头、RGBE数据组成。
文件头如下:
类型 输出格式
char programtype[16]; //#?Radiance/n#Generated by still/n
float gamma; //1.0
float exposure; //1.0
字符串常量 //FORMAT=32-bit_rle_rgbe/n/n
int nWidth, int nHeight //-Y nHeight +X nWidth/n
RGBE数据与HDR FP32(RGB)相互转换公式如下:
1、rgbe->FP32(RGB)
如果e为0, R = G = B = 0.0,否则:
R = r * 2^(e – 128 - 8);
G = g * 2^(e – 128 - 8);
B = b * 2^(e – 128 - 8);
2、FP32(RGB) -> rgbe
v = max(R, G, B);
如果v < 1e-32, r = g = b = e = 0, 否则:
将v用科学计算法表示成 v = m * 2 ^ n ( 0 < m < 1):
r = R * m * 256.0/v;
g = G * m * 256.0/v;
b = B * m * 256.0/v;
e = n + 128;
Still注:
1、我们一般说HDR采用FP32,指的是HDR图象运算时候的内存数据类型,而Radiance RGBE文件采用8bit BYTE类型存储HDR数据。也就是说打开Radiance RGBE文件,要使用上面的公式1将Radiance RGBE文件的8bit BYTE文件数据转换为FP 32的HDR内存数据进行运算;保存为Radiance RGBE文件时,要使用上面的公式2将HDR的FP32内存数据转换为Radiance RGBE的8bit BYTE文件数据进行保存。同理,OpenEXR文件的读写也存在将其FP 16的文件数据到HDR的 FP32图象数据的转换;而下面将要讲的Float Tiff是不需要进行数据转换,直接将HDR的FP 32图象数据保存到TIFF文件中即可。
2、Radiance有多种文件格式,其官方库包含内容比较复杂,所以,实际的读写没有使用其官方库,而是使用了网络上一个简单的C语言读写类,Still并对其进行了部分修改(在文件头写入“Generated by Still”)。
读写类链接地址:http://www.graphics.cornell.edu/~bjw/rgbe.html
官方库链接地址:http://radsite.lbl.gov/radiance/
2.3 FloatTiff文件格式
Tiff文件的扩展名为.tif(.tiff),FloatTiff每个通道为FP32(32bit Float Point)类型,一共3个通道96bpp。用Tiff文件存储HDR数据,直接将HDR的FP32保存到TIFF文件中,有官方库可以利用。下面是Still写的代码样例,HDR数据我采用的是LZW压缩编码:
bool CFloatTiff::Load(const char fileName[], int& width, int& height, float** pixels)
{
TIFF* fp = NULL;
if((fp = TIFFOpen(fileName, "r")) == NULL)
return false;
//获取信息
uint16 bps, spp, datatype, photometric, compression, planarconfig, fillorder;
//每个通道占据的数据位数
if( (TIFFGetField(fp, TIFFTAG_BITSPERSAMPLE, &bps) == 0) || (bps != 32))
return false;
//每个象素的通道数目
if((TIFFGetField(fp, TIFFTAG_SAMPLESPERPIXEL, &spp) == 0) || (spp != 3))
return false;
//每个通道的数据类型
if((TIFFGetField(fp, TIFFTAG_SAMPLEFORMAT, &datatype) == 0) || (datatype != AMPLEFORMAT_IEEEFP))
return false;
//图像的数据采用的颜色模型
if((TIFFGetField(fp, TIFFTAG_PHOTOMETRIC, &photometric) == 0) || (photometric != PHOTOMETRIC_RGB))
return false;
TIFFGetField(fp, TIFFTAG_IMAGEWIDTH, &width);
TIFFGetField(fp, TIFFTAG_IMAGELENGTH, &height);
int num = width * height * 3;
*pixels = new float[num];
if(NULL == *pixels)
return false;
if( TIFFReadEncodedStrip(fp, 0, *pixels, width * height * 3 * 4) == -1)
return false;
TIFFClose(fp);
return true;
}
bool CFloatTiff::Save(const char fileName[], int width, int height, const float* pixels)
{
if(NULL == pixels)
return false;
TIFF *fp = NULL;
if((fp = TIFFOpen(fileName, "w")) == NULL)
return false;
TIFFSetField(fp, TIFFTAG_IMAGEWIDTH, width);
TIFFSetField(fp, TIFFTAG_IMAGELENGTH, height);
TIFFSetField(fp, TIFFTAG_COMPRESSION, COMPRESSION_LZW);//COMPRESSION_DEFLATE;
TIFFSetField(fp, TIFFTAG_PLANARCONFIG, PLANARCONFIG_CONTIG);
TIFFSetField(fp, TIFFTAG_PHOTOMETRIC, PHOTOMETRIC_RGB);
TIFFSetField(fp, TIFFTAG_BITSPERSAMPLE, 32);
TIFFSetField(fp, TIFFTAG_SAMPLESPERPIXEL, 3);
TIFFSetField(fp, TIFFTAG_SAMPLEFORMAT, SAMPLEFORMAT_IEEEFP);
if(TIFFWriteEncodedStrip(fp, 0, const_cast<float*>(pixels), width * height * 3 * 4) == -1)
return false;
TIFFClose(fp);
return true;
}
官方库链接地址:http://www.remotesensing.org/libtiff/
Still注:
1、这篇文章的基础知识大部分来自:《光与影的魔术棒——HDR技术解析》 http://www.cqumzh.cn/topic_show.php?tid=200271。
2、这段时间工作比较忙,关于OpenEXR文件格式的详细介绍需要翻译相关文档,而且这部分内容是05年接触的,重新总结需要一些时间;还有HDR合成、ToneMapping方面的技术下次再奉上。