RGBE(*.hdr, The RADIANCE Picture File Format)文件格式解析

这个文件是一种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编码的数据。

RGBE——r, g, b exp

在正式了解RLE的编码方式之前,我们需要了解一下RGBE。

HDR相比于LDR,最大的差异就是可以储存[0.0, 1.0]以外的数,传统的图像是无符号八位整数,即存放[0, 255]范围的整数,0表示0.0,255表示1.0,如此去映射。很显然,传统方案并不适合存放HDR图片:例如太阳和一个5W的白炽灯,后者甚至连照亮房间的力量都没有,而前者足以照亮一千多万米直径的地球。因此我们需要求助于浮点数

RGBE便是这样做的。如名字所见,RGB是三个通道,E是指数。类似于一般的以2为底数的浮点数,这个表示下面的值:

\\ Output_R = RGBE_R \cdot 2^{RGBE_E - 128 - 8} \\ Output_G = RGBE_G \cdot 2^{RGBE_E - 128 - 8} \\ Output_B = RGBE_B \cdot 2^{RGBE_E - 128 - 8}

当指数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
    }
}

 

自此,我们的工作就做完了~

你可能感兴趣的:(OpenGL,opengl)