HDR图片文件解析

HDR(High Dynamic Range, 高动态范围)。有了HDR,亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节

一.hdr文件头部样例

#?RADIANCE
# Made with FreeImage 3.9.3
FORMAT=32-bit_rle_rgbe
GAMMA=1
EXPOSURE=0

-Y 800 +X 1600

hdr文件前几行代表头部,是以ASCII编码存储的。
每一行以换行符(0x0A)结束。

1,第一行 固定是 #?RADIANCE
2。FORMAT = 32-bit_rle_rgbe 是指图像内容格式是 32位的rbge格式并且用rle压缩(本文章只讨论这个格式)
3。-Y 800 +X 1600 分辨率标识行。表示图像尺寸是 1600*800
Y和X前面的符号是有含义的:
标准图像坐标系如下图,坐标原点在左下角。



-Y M +X N 是渲染器的标准方向,Y前面的负号表示:从左下角向上,Y坐标是递减的,所以本HDR文件坐标原点是左上角开始的。

二、hdr 内容解析

分辨率标识行后面就是像素颜色数据。
这里介绍 [32-bit_rle_rgbe]格式
每一行如下图如示:


RLE行

每一行固定两个2开头,后面二个字节(高字节在前)代表每一行的长度(像素数)(*第3字节的高位必须是0),所以0x0640=1600 与文件头里的分辨率必须一致。后面就是1600个R、1600个G、1600个B, 1600个E 并用 RLE 压缩后的数据。
RLE压缩算法比较简单,可以自行查找。
例: 上图中的0x8E 高位=1,代表(run)重复 14个后续字节(0x9F)
如果高位=0,代表(no-run) 后面不重复字节个数。
以下是读取HDR文件源代码 (nodejs)

/*
    文件头部格式如下:
        #?RADIANCE
        # Made with FreeImage 3.9.3
        FORMAT=32-bit_rle_rgbe
        GAMMA=1
        EXPOSURE=0

        -Y 800 +X 1600
    */
    public static hdrLoad(context: Uint8Array): {
        success: boolean,
        hdrData: Float32Array,
        width: number,
        height: number
    } {
        let dv = new DataView(context.buffer);
        let offset = 0;

        // 第一步,解码HDR文件头部
        let rdLine = this.readLineText(dv, offset);
        offset = rdLine.newOffSet;

        if (rdLine.data !== '#?RADIANCE' && rdLine.data !== '#?RGBE') {
            throw new Error('不正确的HDR文件格式');
        }

        let format = '';
        let gamma = 0;
        let exposure = 0;

        while (true) {
            let rdLine = this.readLineText(dv, offset);
            offset = rdLine.newOffSet;

            if (rdLine.data.startsWith('FORMAT')) {
                format = rdLine.data.substring(7, rdLine.newOffSet)
            } else if (rdLine.data.startsWith('GAMMA')) {
                gamma = parseFloat(rdLine.data.substring(6, rdLine.newOffSet))
            } else if (rdLine.data.startsWith('EXPOSURE')) {
                exposure = parseFloat(rdLine.data.substring(9, rdLine.newOffSet))
            }

            // 空行结束
            if (rdLine.data === '')
                break;
        }

        if (format !== '32-bit_rle_rgbe') {
            throw new Error('只支持格式 [32-bit_rle_rgbe]');
        }


        // 下面一行代码图片尺寸
        //-Y 800 +X 1600
        rdLine = this.readLineText(dv, offset);
        offset = rdLine.newOffSet;
        let t = rdLine.data.split(' ')
        if (t.length !== 4 && t[0] !== '-Y' && t[2] !== '+X') {
            throw new Error('图片尺寸不正确');
        }

        const height = parseInt(t[1])
        const width = parseInt(t[3])

        // 第二步。解码数据
        //let scanline: Float32Array = new Float32Array(0);

        //所有HDR数据的合并
        let hdrDatas = new Float32Array(height * width * 3)

        let hdrrgbeData = new Uint8Array(width * 4)
        let scanlineData = new Uint8Array(width * 4)

        // Read RLE-encoded data
        for (let r = 0; r < height; r++) {

            let c1 = dv.getUint8(offset); offset++;
            let c2 = dv.getUint8(offset); offset++;
            let len = dv.getUint8(offset); offset++;

            if (c1 !== 2 || c2 !== 2 || (len & 0x80) !== 0) {
                //这里 不是RLE
                throw new Error('只支持 RLE压缩格式')
            }
            len <<= 8;
            len |= dv.getUint8(offset); offset++;

            if (len !== width) {
                throw new Error('invalid decoded scanline length')
            }

            let {
                success,
                targetOffset,
                sourceOffset
            }
                = this.rle2scanLine(context, scanlineData, width, offset, undefined, 0);

            if (r === 4) {
                console.debug('读取第4行位置', offset - 4, sourceOffset)
                this.print(dv, offset - 4, sourceOffset)
            }

            offset = sourceOffset;

            this.scanLine2rbge(scanlineData, hdrrgbeData, width, 0, undefined, 0)

            for (let c = 0; c < width; c++) {
                this.rgbe2rgbf(hdrrgbeData, hdrDatas, c * 4, (r * width + c) * 3)
            }
        }

        return {
            success: true,
            hdrData: hdrDatas,
            width: width,
            height: height
        }
    }

    /** */
    public static hdrSave(rbgf32: Float32Array, width: number, height: number): [
        success: boolean,
        hdrData: Uint8Array
    ] {

        //hdrData的最长长度
        let hdrData: Uint8Array = new Uint8Array(height * (width * 4 + 4) + 300)

        //组织HDR文件头部
        const hdrHeader = `#?RADIANCE
# Made with huangyanfei
FORMAT=32-bit_rle_rgbe
GAMMA=1
EXPOSURE=0

+Y ${height} +X ${width}
`;

        const decoder = new TextEncoder();
        const headerBuffer = decoder.encode(hdrHeader)

        hdrData.set(headerBuffer)
        let hdrDataPos: number = headerBuffer.length;

        for (let r = 0; r < height; r++) {
            let rgbeData: Uint8Array = new Uint8Array(width * 4)
            let scanlineData: Uint8Array = new Uint8Array(width * 4)

            for (let c = 0; c < width; c++) {
                this.rgbf2rbge(rbgf32, rgbeData, (r * width + c) * 3, c * 4)
            }
            this.rbge2scanLine(rgbeData, scanlineData, width, 0, undefined, 0)

            let beginPos = hdrDataPos;
            let rlelinev = new DataView(hdrData.buffer, hdrDataPos)
            rlelinev.setUint8(0, 0x02); hdrDataPos++;
            rlelinev.setUint8(1, 0x02); hdrDataPos++;
            rlelinev.setUint16(2, width, false); hdrDataPos += 2;

            let rle = this.scanLine2Rle(scanlineData, hdrData, width * 0, width * 1, hdrDataPos); hdrDataPos = rle.targetOffset;
            rle = this.scanLine2Rle(scanlineData, hdrData, width * 1, width * 2, hdrDataPos); hdrDataPos = rle.targetOffset;
            rle = this.scanLine2Rle(scanlineData, hdrData, width * 2, width * 3, hdrDataPos); hdrDataPos = rle.targetOffset;
            rle = this.scanLine2Rle(scanlineData, hdrData, width * 3, width * 4, hdrDataPos); hdrDataPos = rle.targetOffset;

            if (r === 4) {
                console.debug('写入第4行位置', beginPos, hdrDataPos)
                this.print(new DataView(hdrData.buffer), beginPos, hdrDataPos)
            }
        }

        return [
            true,
            hdrData.slice(0, hdrDataPos)
        ]
    }

    // 一行进行 RLE 压缩,写入target 指定位置,返回结束位置
    // pixelCount 像素个数,所以target的长度是 pixelCount * 4
    public static scanLine2Rle(source: Uint8Array, target: Uint8Array, sourceOffset: number = 0, sourceEnd: number = source.length, targetOffset: number = 0): {
        success: boolean,
        targetOffset: number
    } {
        const MINRUNLENGTH = 4;
        let cur, beg_run, run_count, old_run_count, norun_count: number;
        let buf: Array<number> = [0, 0];

        cur = sourceOffset;

        let woffset = targetOffset;
        const numbytes = sourceEnd;
        while (cur < numbytes) {
            beg_run = cur;
            run_count = old_run_count = 0;
            while ((run_count < MINRUNLENGTH) && (beg_run < numbytes)) {
                beg_run += run_count;
                old_run_count = run_count;
                run_count = 1;

                while ((source[beg_run] === source[beg_run + run_count])
                    && ((beg_run + run_count) < numbytes) && (run_count < 127))
                    run_count++
            }

            if ((old_run_count > 1) && (old_run_count === beg_run - cur)) {
                buf[0] = 128 + old_run_count
                buf[1] = source[cur]

                target.set(buf, woffset); woffset += 2;

                cur = beg_run;
            }

            while (cur < beg_run) {
                norun_count = beg_run - cur;
                if (norun_count > 128)
                    norun_count = 128;

                buf[0] = norun_count;
                target.set([buf[0]], woffset); woffset += 1;
                target.set(source.subarray(cur, cur + norun_count), woffset); woffset += norun_count;

                cur += norun_count;
            }

            if (run_count >= MINRUNLENGTH) {
                buf[0] = 128 + run_count;
                buf[1] = source[beg_run];

                target.set(buf, woffset); woffset += 2;

                cur += run_count
            }
        }

        return { success: true, targetOffset: woffset };
    }

    public static scanLine2rbge(source: Uint8Array, target: Uint8Array, pixelCount: number, sourceOffset: number = 0, sourceEnd: number = source.length, targetOffset: number = 0) {
        for (let i = 0; i < pixelCount; i++) {
            target[targetOffset + i * 4 + 0] = source[sourceOffset + pixelCount * 0 + i]
            target[targetOffset + i * 4 + 1] = source[sourceOffset + pixelCount * 1 + i]
            target[targetOffset + i * 4 + 2] = source[sourceOffset + pixelCount * 2 + i]
            target[targetOffset + i * 4 + 3] = source[sourceOffset + pixelCount * 3 + i]
        }
    }

    public static rbge2scanLine(source: Uint8Array, target: Uint8Array, pixelCount: number, sourceOffset: number = 0, sourceEnd: number = source.length, targetOffset: number = 0) {
        for (let i = 0; i < pixelCount; i++) {
            target[targetOffset + pixelCount * 0 + i] = source[sourceOffset + i * 4 + 0]
            target[targetOffset + pixelCount * 1 + i] = source[sourceOffset + i * 4 + 1]
            target[targetOffset + pixelCount * 2 + i] = source[sourceOffset + i * 4 + 2]
            target[targetOffset + pixelCount * 3 + i] = source[sourceOffset + i * 4 + 3]
        }
    }

    // 一行数据 RLE解压
    public static rle2scanLine(source: Uint8Array, target: Uint8Array, pixelCount: number, sourceOffset: number = 0, sourceEnd: number = source.length, targetOffset: number = 0): {
        success: boolean,
        targetOffset: number,
        sourceOffset: number,
        rleUnits: Array<any>
    } {
        let readOffset = sourceOffset
        let readEnd = sourceEnd;
        let count = 0

        let finishUnit = 0;
        let woffset = targetOffset;
        let rt = []
        while (readOffset < readEnd) {
            let buf = source.subarray(readOffset, readOffset + 2); readOffset += 2
            if (buf[0] > 128) {
                //重复
                count = buf[0] - 128;

                const runData = new Uint8Array(count).fill(buf[1])
                target.set(runData, woffset);

                rt.push({ F: 'R', L: count, I: woffset, header: this.arrayToStr(buf, 0, 2), data: [buf[1]] })

                woffset += count;

            } else {
                // 不重复
                count = buf[0]

                const norunData = source.slice(readOffset - 1, readOffset - 1 + count)

                target.set(norunData, woffset);

                rt.push({ F: 'N', L: count, I: woffset, header: this.arrayToStr(buf, 0, 1), data: this.arrayToStr(norunData) })

                readOffset += count - 1;
                woffset += count;
            }

            finishUnit++;

            if (woffset - targetOffset >= pixelCount * 4)
                break;
        }

        if (woffset - targetOffset !== pixelCount * 4) {
            throw (`RLE解压失败, 目标生成字节数=${woffset - targetOffset}, 期望字节数=${pixelCount * 4}`)
        }

        return {
            success: true,
            targetOffset: woffset,
            sourceOffset: readOffset,
            rleUnits: rt
        }
    }

    //将HDR格式(4字节,RGBE)转成(RBG32F) 一共12字节
    public static rgbe2rgbf(source: Uint8Array, target: Float32Array, sourceOffset: number = 0, targetOffset: number = 0) {
        const e = source[sourceOffset + 3]
        const r = source[sourceOffset]
        const g = source[sourceOffset + 1]
        const b = source[sourceOffset + 2]

        if (e !== 0) {
            //指数
            let f1: number = 0;
            f1 = 1.0 * Math.pow(2, e - (128 + 8));

            target[targetOffset] = r * f1;
            target[targetOffset + 1] = g * f1;
            target[targetOffset + 2] = b * f1;
        }
        else {
            target[targetOffset] = 0;
            target[targetOffset + 1] = 0;
            target[targetOffset + 2] = 0;
        }
    }

    // 将HDR格式 RBG32F(12字节一组)转成 RGBE(4字节一组)
    public static rgbf2rbge(source: Float32Array, target: Uint8Array, sourceOffset: number = 0, targetOffset: number = 0) {
        let v: number = 0;
        let e: number = 0;

        const r = source[sourceOffset];
        const g = source[sourceOffset + 1];
        const b = source[sourceOffset + 2];

        v = Math.max(r, g, b);
        if (v < Math.pow(10, -32)) {
            target[targetOffset] = 0;
            target[targetOffset + 1] = 0;
            target[targetOffset + 2] = 0;
            target[targetOffset + 3] = 0;
        } else {
            let [v1, e] = MathExtend.frexp(v);
            v1 = v1 * 256 / v;

            target[targetOffset] = r * v1;
            target[targetOffset + 1] = g * v1;
            target[targetOffset + 2] = b * v1;
            target[targetOffset + 3] = e + 128;
        }
    }

中间每一行转换后的数据格式如下


RGBE格式

自定义一个数学库

/**数学函数库扩展 */
export class MathExtend {
    // 将一个数字转化为 尾数 * 2 的 n次方
    public static frexp(input: number): [number, number] {
        const sign = Math.sign(input);
        input *= sign;

        const exponent = Math.floor(Math.log2(input));
        const mantissa = input / Math.pow(2, exponent + 1);

        return [mantissa, exponent + 1]
    }
}

转换后字节内容如下:


HDR的RGB

也就是每个通道4字节的float,一个像素12个字节。
经过以上两步生成的RGB数据可以作为OpenGL的纹理数据了。

三、验证转换是否正确

我们要把以上RGB格式写入BMP文件。
BMP格式是每个通道1字节(范围是0~255),我们要把HDR转换为LDR。

Reinhard 色调映射 转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程
每像素12字节变成 每像素3字节
中间加入GAMMA校准

public static ReinhardToneMap(hdrData: Uint8Array, Exposure = 0, islittleEndian = true): Uint8Array {
        let ldrContext = new Uint8Array(hdrData.length / 4);
        const ldrView = new DataView(ldrContext.buffer);
        const hdrView = new DataView(hdrData.buffer);

        const gamma = 2.2

        for (let i = 0; i < ldrContext.length; i += 3) {
            const hr = hdrView.getFloat32(i * 4,islittleEndian )
            const hg = hdrView.getFloat32((i + 1) * 4,islittleEndian )
            const hb = hdrView.getFloat32((i + 2) * 4,islittleEndian )

            const mhr = Math.round(Math.pow(hr / (hr + 1.0), 1.0 / gamma) * 0xFF)
            const mhg = Math.round(Math.pow(hg / (hg + 1.0), 1.0 / gamma) * 0xFF)
            const mhb = Math.round(Math.pow(hb / (hb + 1.0), 1.0 / gamma) * 0xFF)

            ldrView.setUint8(i, mhr)
            ldrView.setUint8(i + 1, mhg)
            ldrView.setUint8(i + 2, mhb)
        }

        return ldrContext
    }

生成BMP24图像

    // 生成24位BPM文件内容
    // data数据是每像素3字节RGB数据
    public static genBMP24(width: number, height: number, sourceData: Uint8Array): Uint8Array {
        let fileContent = new Uint8Array(width * height * 3 + 54)
        const dt = new DataView(fileContent.buffer)
        let offset = 0

        //每行字节数,对字节对齐后
        let LineByteCnt = (((width * 24) + 31) >> 5) << 2;

        //bmp文件头 14字节
        dt.setUint8(offset, 0x42); offset++;   //B
        dt.setUint8(offset, 0x4D); offset++;   //M
        dt.setUint32(offset, LineByteCnt * height + 54, true); offset += 4; //文件大小,每个像素3字节 + 54个文件头
        offset += 4; //预留 
        dt.setUint32(offset, 54, true); offset += 4;    //无调色版信息

        // bmp文件头 40字节
        dt.setUint32(offset, 40, true); offset += 4;   //BITLAPIHFOIEADE结构的宇数 = 40
        dt.setUint32(offset, width, true); offset += 4;  //
        dt.setUint32(offset, -height, true); offset += 4;
        dt.setUint16(offset, 1, true); offset += 2; //为目标设备说明颜色平面数,
        dt.setUint16(offset, 24, true); offset += 2;  //说明比特数/像素,其值为1、4、816、24或32
        dt.setUint32(offset, 0, true); offset += 4;  //压缩的类型。
        dt.setUint32(offset, LineByteCnt * height, true); offset += 4;  //图像的大小
        offset += 16;  //后面不用设置

        //按行拷贝数据
        for (let r = 0; r < height; r++) {
            for (let c = 0; c < width; c++) {
                //因为BMP 存储为BGR
                dt.setUint8(offset + r * LineByteCnt + c * 3 + 2, sourceData[r * width * 3 + c * 3 + 0])
                dt.setUint8(offset + r * LineByteCnt + c * 3 + 1, sourceData[r * width * 3 + c * 3 + 1]);
                dt.setUint8(offset + r * LineByteCnt + c * 3 + 0, sourceData[r * width * 3 + c * 3 + 2]);
            }
        }

        return fileContent
    }

四、效果


对比

上图是HDR,下图是BMP格式。对比图像细节,BMP有明显的锯齿。因为从HDR转换为LDR是精度有损失了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 摘自知乎:https://zhuanlan.zhihu.com/p/90093872[https://zhuanl...
    miniminiming阅读 930评论 0 0
  • 第一章:视频编码必备知识点(以Davinci及QT格式为例) 一、封装格式VS编码格式 在DaVinci Reso...
    夏源XY阅读 11,287评论 0 18
  • JPEG文件的存储格式有很多种,但最常用的是JFIF格式,即JPEG File Interchange Forma...
    hehtao阅读 8,940评论 1 7
  • BMP文件格式 BMP 取自 Bitmap 的缩写。BMP 文件格式,也被称为位图图像文件或设备无关位图(DIB,...
    雷震西山阅读 2,367评论 0 1
  • 最近在看图像的压缩,就想着先实现一个jpeg文件的解码。本来以为这种资料在网上会一搜一大堆,但搜了之后才发现很多网...
    月夢書阅读 10,459评论 6 14