Original image is a screengrab of the Sverige Televisions (SVT) HDR Natural Complexity Test
Should look the same as HLG Original
Should look the same as PQ tonemapped to sRGB
Should look the same as HLG tonemapped to sRGB
Should look the same as PQ tonemapped to BT.2020 Linear
Should look the same as HLG tonemapped to BT.2020 Linear
Should react to ambient light button and look the same as the reactive PQ tonemapped to sRGB
Should react to ambient light button and look the same as the reactive HLG tonemapped to sRGB
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;
}
}
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);
}
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;
}
}
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);
}