这个文件是一种HDR格式。最近玩OpenGL想要做基于图像的光照,但是——我得弄个HDR图片。最终,我选择了Radiance RGBE格式(下面称作rgbe格式)。因为能在这里白嫖到一些。在不想用库的情况下,于是便参考std_image与RGBE的官方解释,编写了这篇文章。
不同于PNG格式那样分块,rgbe格式只包括两个部分。一个很小的文件头,和一段通常很大的文件数据。文件数据是RLE压缩的:
文件头很简单,它是一段ASCII字符串。文件头里面的字符串是一行一行的,可以有空行,以#打头的是注释,可以出现在任何位置,应该被忽略。同时,最开始的几个字符是"#?RADIANCE"。紧接着是XXX=XXX这样的文件信息。我们随便找一个rgbe文件格式,可以看到它的文件头:
#?RADIANCE
# Made with 100% pure HDR Shop
FORMAT=32-bit_rle_rgbe
EXPOSURE= 1.0000000000000
-Y 128 +X 256
其中,各个数据含义如下:
FORMAT | 文件数据格式, 32-bit_rle_rgbe表示RLE编码的数据 |
EXPOSURE | 曝光度,一般是1,不管它了吧~ |
-Y | 指定图像高度 |
+X | 指定图像宽度 |
现在,开始干正事——解析rgbe文件,把它转换为[float32 : R], [float32 : G], [float32 : B]的数据。因为float是4字节的,所以输出结果也是对齐的。不需要有步长处理。这样就可以直接送入OpenGL了。
现在来梳理一下我们的程序——首先,需要一个函数来检查文件头。假定fs是我的文件读取流(下同)的话,数一数,文件头是是一个字符('#', '?', 'R', 'A', 'D', 'I', 'A', 'N', 'C', 'E', '\n'),因此:
bool HDRImg::isRadianceFile(BuffISteam &fs) noexcept
{
bool r;
char tmp[12];
r = fs.Read(tmp, 11); // 尝试读取文件头
tmp[11] = '\0'; // 封闭字符串
if (r == false || strcmp("#?RADIANCE\n", tmp) != 0) // 读取失败或者文件头不正确
{
return false;
}
return true;
}
接着,为了方便解析程序,我们需要一个函数用于取回每一行的数据。方便起见,这个函数应该忽略空行和注释:
bool HDRImg::getToken(BuffISteam &fs, char *buff, size_t buffsz) noexcept
{
bool ret = true;
char tmp, *pdat;
retry:
pdat = buff;
while (pdat < buff + buffsz - 1)
{
if (fs.Read(&tmp, 1) == false)
{
ret = false;
break;
}
if (tmp == '\n' || tmp == '\0')
{
*pdat = '\0'; // 封闭字符串
break;
}
else {
*pdat = tmp;
pdat += 1;
}
}
assert(pdat < buff + buffsz);
if (tmp != '\0' && (buff[0] == '#' || buff[0] == '\0')) // 注释行或者空行, 重新再来一次
{
goto retry;
}
return ret;
}
现在处理文件头的部分准备就绪。现在就可以检查文件格式是否正确了——先使用isRadianceFile,然后再用getToken拿到FORMAT、-Y、+X,得到数据格式和尺寸:
bool HDRImg::LoadFile(const tchar *path)
{
char buff[128];
int width, height;
if (fs.LoadData(path) == false)
{
return false;
}
else if (isRadianceFile(fs) == false)
{
return false;
}
//
if (getToken(fs, buff, 128) == false)
{
return false;
}
if (strcmp("FORMAT=32-bit_rle_rgbe", buff) != 0)
{
return false; // 不支持的文件格式
}
//
do // 连续读取, 直到找到-Y XXX这个尺寸信息
{
if (getToken(fs, buff, 128) == false)
{
return false;
}
} while (buff[0] != '-' || buff[1] != 'Y');
int sz = sscanf_s(buff, "-Y %d +X %d", &height, &width);
if(sz != 2) // 不符合规范的尺寸数据
{
return false;
}
//
// TODO 处理RLE编码的数据
return true;
}
OK~试试能不能正常工作。接着,我们开始处理RLE编码的数据。
在正式了解RLE的编码方式之前,我们需要了解一下RGBE。
HDR相比于LDR,最大的差异就是可以储存[0.0, 1.0]以外的数,传统的图像是无符号八位整数,即存放[0, 255]范围的整数,0表示0.0,255表示1.0,如此去映射。很显然,传统方案并不适合存放HDR图片:例如太阳和一个5W的白炽灯,后者甚至连照亮房间的力量都没有,而前者足以照亮一千多万米直径的地球。因此我们需要求助于浮点数。
RGBE便是这样做的。如名字所见,RGB是三个通道,E是指数。类似于一般的以2为底数的浮点数,这个表示下面的值:
当指数E为0时,则结果的三个通道都是0,不然三个通道表示上述值。我们可以借助ldexp来计算2的n次幂,从而来转换三个通道的值。假设输出是一个flaot[3],我们可以这样编写代码把RGBE数据转换为RGB(float32):
// 将rgbe数据转换为rgb, 存放格式如下:
// float[0] : R
// float[1] : G
// float[2] : B
// 输入按照RGBE顺序存放
void HDRImg::rgbe2float32(const byte *rgbe, float *out) noexcept
{
if (rgbe[3] == 0) // 指数位是0, rgb都是0
{
out[0] = 0.0f;
out[1] = 0.0f;
out[2] = 0.0f;
}
else {
const int E = (int)rgbe[3] - 128 - 8; // 指数位的值
const double P = ldexp(2.0, E); // 2的E次幂的结果
out[0] = (float)((double)rgbe[0] * P); // 计算三个通道的值
out[1] = (float)((double)rgbe[1] * P);
out[2] = (float)((double)rgbe[2] * P);
}
}
类似地,我们假设当前写入的数据位置是curpdata,来编写一个读取数据的函数:
bool HDRImg::readRGBEPixel(size_t count)
{
size_t i;
byte temp[4];
for (i = 0u; i < count; i += 1)
{
if (fs.Read(temp, 4) == false)
{
return false;
}
rgbe2float32(temp, curpdata);
curpdata += 3; // rgb三个通道, 所以是+=3
}
return true;
}
RLE就是游程编码。操作很简单,就是把连续出现的n个字符编码为(n, 这个字符)。例如JuuuuYaaaaannnn。就可以这样编码:
J4uY5a4n
rgbe文件中的编码我没有找到文档,因此直接按照官方代码修改了一个:
bool HDRImg::readData()
{
int i;
bool ret = true;
byte temp[4], *scanline;
scanline = new byte[4 * imgw];
if (imgw < 8 || imgw > 0x7fff) // 未编码数据直接读取
{
ret = readRGBEPixel((size_t)(imgw * imgh));
}
else { // 读取所有数据, 按照扫描线读取
for (i = 0; i < imgh; i += 1)
{
if ((ret = fs.Read(temp, 4)) == false)
{
break; // 无法读取数据
}
if (temp[0] != 2 || temp[1] != 2 || (temp[2] & 0x80))
{ // 该部分未使用rle编码
rgbe2float32(temp, curpdata);
curpdata += 3;
ret = readRGBEPixel((size_t)(imgw * imgh - 1)); // (已经读取过一个像素了, 所以 - 1)
break;
}
if ((((int)temp[2]) << 8 | temp[3]) != imgw) // 错误的编码
{
ret = false;
break;
}
byte buff[2];
for (int i = 0; i < 4; i += 1) // 读取四个通道的数据到scanline
{
byte *pdat = scanline + (i + 0) * imgw;
byte *pend = scanline + (i + 1) * imgw;
while (pdat < pend)
{
if ((ret = fs.Read(buff, 2)) == false)
{
ret = false;
goto err;
}
if (buff[0] > 128) // 一小块相同值的数据
{
int count = (int)buff[0] - 128;
if ((count == 0) || (count > pend - pdat))
{ // 不对劲的块
ret = false;
goto err;
}
while (count-- > 0)
{
*pdat = buff[1];
pdat += 1;
}
}
else { // (啥也不是)
int count = (int)buff[0];
if ((count == 0) || (count > pend - pdat))
{ // 不对劲的块
ret = false;
goto err;
}
*pdat = buff[1];
pdat += 1;
count -= 1;
if (count > 0)
{
if (fs.Read(pdat, (size_t)count) == false)
{
ret = false;
goto err;
}
pdat += count;
}
}
}
}
rgbe2float32(scanline, (size_t)imgw); // 把这行rgbe转换为float
}
}
err:
delete scanline;
return ret;
}
注意scanline是imgw个R、imgw个G这样存放的,使用下面的函数转换:
// rgbe -> rgb
// 输入格式
// pdat:
// [
// R, R, ......(count个),
// G, G, ......(count个),
// B, B, ......(count个),
// E, E, ......(count个),
// ]
void HDRImg::rgbe2float32(const byte *pdat, size_t count) noexcept
{
byte tmp[4];
for (size_t i = 0u; i < count; i += 1)
{
tmp[0] = pdat[i + count * 0];
tmp[1] = pdat[i + count * 1];
tmp[2] = pdat[i + count * 2];
tmp[3] = pdat[i + count * 3]; // 调整出rgbe顺序的数据
rgbe2float32(tmp, curpdata);
curpdata += 3; // rgb三个通道, +=3
}
}
自此,我们的工作就做完了~