Tone mapping demonstration

Original image is a screengrab of the Sverige Televisions (SVT) HDR Natural Complexity Test

HDR Conversions

HLG Original Image

PQ Original Image

PQ to HLG

Should look the same as HLG Original

HDR to SDR Tonemapping

HLG to sRGB - W3C canvas simple tonemapping method

Should look the same as PQ tonemapped to sRGB

PQ to HLG to sRGB - W3C canvas simple tonemapping method

Should look the same as HLG tonemapped to sRGB

HLG to Rec2020 linear - W3C canvas simple tonemapping method

Should look the same as PQ tonemapped to BT.2020 Linear

PQ to HLG to Rec2020 linear - W3C canvas simple tonemapping method

Should look the same as HLG tonemapped to BT.2020 Linear

HLG to sRGB - W3C canvas complex tonemapping method (applied on Y so more accurate saturation and hue)

With Ambient Light Correction

W3C canvas tonemapping method with ambient light correction (HLG)

Should react to ambient light button and look the same as the reactive PQ tonemapped to sRGB

W3C canvas tonemapping method with ambient light correction (PQ)

Should react to ambient light button and look the same as the reactive HLG tonemapped to sRGB

Code Snippets

PQ to HLG


const twoPowerBitDepthMinusOne = 255;

export function pqToHlg(imageData)  {
    const pqMax = 10000.0;
    const lw = 1000.0;
    
    for (let i = 0; i < imageData.data.length; i += 4) {
        const rp = imageData.data[i]     / twoPowerBitDepthMinusOne;
        const gp = imageData.data[i + 1] / twoPowerBitDepthMinusOne;
        const bp = imageData.data[i + 2] / twoPowerBitDepthMinusOne;

        // invert PQ
        let r = tfe.pqEOTF(rp);
        let g = tfe.pqEOTF(gp);
        let b = tfe.pqEOTF(bp);
        
        // Scale
        r = (pqMax / lw) * r;
        g = (pqMax / lw) * g;
        b = (pqMax / lw) * b;

        // Remove burnt in OOTF
        [r, g, b] = tfe.hlgOOTFInverse(r, g, b, 1.20);

        // Apply HLG OETF
        r = tfe.hlgOETF(r);
        g = tfe.hlgOETF(g);
        b = tfe.hlgOETF(b);

        [r, g, b] = cs.clipGamut(r, g, b);

        imageData.data[i]     = twoPowerBitDepthMinusOne * r;
        imageData.data[i + 1] = twoPowerBitDepthMinusOne * g;
        imageData.data[i + 2] = twoPowerBitDepthMinusOne * b;
    }
}
      

Using HLG as a simple tone-mapping operator


export function bt2100HLGTonemaptosRGBDisplay_w3cSimpleVersion(imageData) {
    for (let i = 0; i < imageData.data.length; i += 4) {
        const rp = imageData.data[i]     / twoPowerBitDepthMinusOne;
        const gp = imageData.data[i + 1] / twoPowerBitDepthMinusOne;
        const bp = imageData.data[i + 2] / twoPowerBitDepthMinusOne;

        // invert gamma
        let r = Math.pow(rp, 2.2);
        let g = Math.pow(gp, 2.2);
        let b = Math.pow(bp, 2.2);

        // Colour space conversion
        const m = cs.Rec2020ToRec709;

        [r, g, b] = cs.applyColourMatrix(r, g, b, m);

        // put pixel back
        [r, g, b] = cs.clipGamut(r, g, b);

        imageData.data[i]     = twoPowerBitDepthMinusOne * Math.pow(r, 1 / 2.2);
        imageData.data[i + 1] = twoPowerBitDepthMinusOne * Math.pow(g, 1 / 2.2);
        imageData.data[i + 2] = twoPowerBitDepthMinusOne * Math.pow(b, 1 / 2.2);
    }
}

export function bt2100HLGTonemaptoRec2020Linear_w3cSimpleVersion(imageData) {
    for (let i = 0; i < imageData.data.length; i += 4) {
        const rp = imageData.data[i]     / twoPowerBitDepthMinusOne;
        const gp = imageData.data[i + 1] / twoPowerBitDepthMinusOne;
        const bp = imageData.data[i + 2] / twoPowerBitDepthMinusOne;

        // invert gamma
        let r = Math.pow(rp, 2.2);
        let g = Math.pow(gp, 2.2);
        let b = Math.pow(bp, 2.2);

        // put pixel back
        [r, g, b] = cs.clipGamut(r, g, b);

        imageData.data[i]     = twoPowerBitDepthMinusOne * r;
        imageData.data[i + 1] = twoPowerBitDepthMinusOne * g;
        imageData.data[i + 2] = twoPowerBitDepthMinusOne * b;
    }
}

export function bt2100PqTosRGBviaHlg(imageData) {
    pqToHlg(imageData);
    bt2100HLGTonemaptosRGBDisplay_w3cSimpleVersion(imageData);
}

export function bt2100PqToRec2020LinearviaHlg(imageData) {
    pqToHlg(imageData);
    bt2100HLGTonemaptoRec2020Linear_w3cSimpleVersion(imageData);
}
      

Using HLG as a simple tone-mapping operator with operation on luminance


      
export function bt2100HLGTonemaptosRGBDisplay_w3cComplexVersion(imageData) {
    for (let i = 0; i < imageData.data.length; i += 4) {
        const rp = imageData.data[i]     / twoPowerBitDepthMinusOne;
        const gp = imageData.data[i + 1] / twoPowerBitDepthMinusOne;
        const bp = imageData.data[i + 2] / twoPowerBitDepthMinusOne;

        // invert gamma
        let [l, u, v] = cs.calcBt2020toLuv(rp, gp, bp);
        l = Math.pow(l, 2.20);
        const [r, g, b] = cs.calcLuvToBt2020(l, u, v);
        
        // Colour space conversion
        const m = cs.Rec2020ToRec709;

        [r, g, b] = cs.applyColourMatrix(r, g, b, m);

        // put pixel back
        // apply gamma
        let [l, u, v] = cs.calcBt2020toLuv(r, g, b);
        l = Math.pow(l, 1.0 / 2.20);
        const [r, g, b] = cs.calcLuvToBt2020(l, u, v);
        [r, g, b] = cs.clipGamut(r, g, b);

        imageData.data[i]     = twoPowerBitDepthMinusOne * r;
        imageData.data[i + 1] = twoPowerBitDepthMinusOne * g;
        imageData.data[i + 2] = twoPowerBitDepthMinusOne * b;
    }
} 
        

Ambient Light Adaptation


export function bt2390_AmbientCorrection(imageData, ambientLevel) {
    for (let i = 0; i < imageData.data.length; i += 4) {
        const rp = imageData.data[i]     / twoPowerBitDepthMinusOne;
        const gp = imageData.data[i + 1] / twoPowerBitDepthMinusOne;
        const bp = imageData.data[i + 2] / twoPowerBitDepthMinusOne;

        /* calculate ambient level compensation */
        const ambientGamma = 1.0 - (0.076 * Math.log10(ambientLevel / 5.0));

        /* apply ambient level compensation */
        let [l, u, v] = cs.calcBt2020toLuv(rp, gp, bp);
        l = Math.pow(l, ambientGamma);
        const [r, g, b] = cs.calcLuvToBt2020(l, u, v);

        imageData.data[i]     = twoPowerBitDepthMinusOne * r;
        imageData.data[i + 1] = twoPowerBitDepthMinusOne * g;
        imageData.data[i + 2] = twoPowerBitDepthMinusOne * b;
    }
}

export function bt2100HLGTonemaptosRGB_w3cAmbientCorrection(imageData, ambientLevel) {
    bt2390_AmbientCorrection(imageData, ambientLevel);
    bt2100HLGTonemaptosRGBDisplay_w3cSimpleVersion(imageData);
}

export function bt2100PQTonemaptosRGB_w3cAmbientCorrection(imageData, ambientLevel) {
    bt2390_AmbientCorrection(imageData, ambientLevel);
    bt2100PqTosRGBviaHlg(imageData);
}