function pqEOTF(ep) { const a = Math.pow(ep, 1/78.84375); return Math.pow(Math.max(a - 0.8359375, 0)/(18.8515625 - 18.6875*a), 1/0.1593017578125); } function pqInverseEOTF(y) { const m1 = 0.1593017578125; const c1 = 0.8359375; const c2 = 18.8515625; const c3 = 18.6875; const m2 = 78.84375; const a = Math.pow(y, m1); return Math.pow((c1 + c2*a)/(1 + c3*a), m2); } function P3D65ToRec709(r, g, b) { const m = [[1.22494017628056, -0.22494017628056, -0.00000000000001], [-0.04205695470969, 1.04205695470970, 0.00000000000002], [-0.01963755459033, -0.07863604555064, 1.09827360014097]]; const rt = r * m[0][0] + g * m[0][1] + b * m[0][2]; const gt = r * m[1][0] + g * m[1][1] + b * m[1][2]; const bt = r * m[2][0] + g * m[2][1] + b * m[2][2]; return [rt, gt, bt]; } function itu2408ToneMap(imageData) { function calcE3(e1, ks, b, maxL) { let e2; if (e1 < ks) { e2 = e1; } else { const te1 = (e1 - ks)/(1 - ks); const te12 = Math.pow(te1, 2); const te13 = Math.pow(te1, 3); e2 = ks * (2 * te13 - 3 * te12 + 1) + (1 - ks) * (te13 - 2 * te12 + te1) + maxL * (-2 * te13 + 3 * te12); } return e2 + b * Math.pow(1 - e2, 4); } /* constants */ const hdrW = 1000; const hdrB = 0; const sdrW = 203; const sdrB = 0; const pqW = pqInverseEOTF(hdrW / 10000); const pqB = pqInverseEOTF(hdrB / 10000); const pqRange = pqW - pqB; const minL = (pqInverseEOTF(sdrB / 10000) - pqB) / pqRange; const maxL = (pqInverseEOTF(sdrW / 10000) - pqB) / pqRange; const ks = 1.5 * maxL - 0.5; const b = minL; for (let i = 0; i < imageData.data.length; i += 4) { const r_pq = imageData.data[i] / 255; const g_pq = imageData.data[i + 1] / 255; const b_pq = imageData.data[i + 2] / 255; /* step 1 */ const e1r = (r_pq - pqB) / pqRange; const e1g = (g_pq - pqB) / pqRange; const e1b = (b_pq - pqB) / pqRange; /* step 3 - 5 */ const e4r = calcE3(e1r, ks, b, maxL) * pqRange + pqB; const e4g = calcE3(e1g, ks, b, maxL) * pqRange + pqB; const e4b = calcE3(e1b, ks, b, maxL) * pqRange + pqB; /* linearize */ let rt = 10000 * pqEOTF(e4r) / sdrW; let gt = 10000 * pqEOTF(e4g) / sdrW; let bt = 10000 * pqEOTF(e4b) / sdrW; /* color space convert */ [rt, gt, bt] = P3D65ToRec709(rt, gt, bt); /* put pixel back */ imageData.data[i] = 255 * Math.pow(rt, 1/2.4); imageData.data[i + 1] = 255 * Math.pow(gt, 1/2.4); imageData.data[i + 2] = 255 * Math.pow(bt, 1/2.4); } } function ituToneMap(imageData) { for (let i = 0; i < imageData.data.length; i += 4) { const r_pq = imageData.data[i] / 255; const g_pq = imageData.data[i + 1] / 255; const b_pq = imageData.data[i + 2] / 255; /* constants */ const l_hdr = 1000; const l_sdr = 100; const rho_h = 1 + 32 * Math.pow(l_hdr / 10000, 1/2.4) const rho_s = 1 + 32 * Math.pow(l_sdr / 10000, 1/2.4) /* invert PQ and scale by mastering display luminance */ let r = pqEOTF(r_pq) * 10000 / l_hdr; let g = pqEOTF(g_pq) * 10000 / l_hdr; let b = pqEOTF(b_pq) * 10000 / l_hdr; /* Non-linear transfer function */ const rp = Math.pow(r, 1/2.4); const gp = Math.pow(g, 1/2.4); const bp = Math.pow(b, 1/2.4); /* Luma */ const yp = 0.2627 * rp + 0.6780 * gp + 0.0593 * bp; /* Tone mapping step 1 */ const ypp = Math.log(1 + (rho_h - 1) * yp)/Math.log(rho_h); /* Tone mapping step 2 */ let ypc; if (ypp <= 0.7399) { ypc = 1.0770 * ypp; } else if (ypp < 0.9909) { ypc = -1.1510 * Math.pow(ypp, 2) + 2.7811 * ypp - 0.6302 } else { ypc = 0.5 * ypp + 0.5; } /* Tone mapping step 3 */ const yps = (Math.pow(rho_s, ypc) - 1) / (rho_s - 1); /* Colour difference signals */ const cpbt = yps/(1.1 * yp) * (bp - yp) / 1.8814; const cprt = yps/(1.1 * yp) * (rp - yp) / 1.4746; /* Adjusted luma component */ const ypt = yps - Math.max(0.1 * cprt, 0); /* convert to rgb */ const a = 0.2627; const c = 0.0593; let rt = ypt + (2 - 2*a) * cprt; let gt = ypt - ((2*c-2*c*c)/(1-a-c))*cpbt - ((2*a-2*a*a)/(1-a-c))*cprt; let bt = ypt + (2 - 2*c) * cpbt; /* invert gamma */ rt = Math.pow(rt, 2.4); gt = Math.pow(gt, 2.4); bt = Math.pow(bt, 2.4); /* color space convert */ [rt, gt, bt] = P3D65ToRec709(rt, gt, bt); /* put pixel back */ imageData.data[i] = 255 * Math.pow(rt, 1/2.4); imageData.data[i + 1] = 255 * Math.pow(gt, 1/2.4); imageData.data[i + 2] = 255 * Math.pow(bt, 1/2.4); } } function st209410ToneMap(imageData) { const sdrL = 100; let minPQ = 10; let maxPQ = -1; let avgPQ = 0; for (let i = 0; i < imageData.data.length; i += 4) { const r_pq = imageData.data[i] / 255; const g_pq = imageData.data[i + 1] / 255; const b_pq = imageData.data[i + 2] / 255; const maxRGB = Math.max(r_pq, g_pq, b_pq); minPQ = Math.min(minPQ, r_pq, g_pq, b_pq); maxPQ = Math.max(maxPQ, maxRGB); avgPQ += maxRGB; } avgPQ = avgPQ / (imageData.data.length / 4); document.getElementById("minPQ").innerText = minPQ.toFixed(3); document.getElementById("avgPQ").innerText = avgPQ.toFixed(3); document.getElementById("maxPQ").innerText = maxPQ.toFixed(3); // console.log(`min: ${minPQ}, avg: ${avgPQ}, max: ${maxPQ}`); /* constants */ const x1 = 10000 * pqEOTF(minPQ); const x2 = 10000 * pqEOTF(avgPQ); const x3 = 10000 * pqEOTF(maxPQ); // console.log(`x1: ${x1}, x2: ${x2}, x3: ${x3}`); const y1 = 0.1; const y3 = sdrL; const y2 = Math.sqrt(x2 * Math.sqrt(y3 * y1)); const a11 = x2 * x3 * (y2 - y3); const a12 = x1 * x3 * (y3 - y1); const a13 = x1 * x2 * (y1 - y2); const a21 = x3 * y3 - x2 * y2; const a22 = x1 * y1 - x3 * y3; const a23 = x2 * y2 - x1 * y1; const a31 = x3 - x2; const a32 = x1 - x3; const a33 = x2 - x1; const alpha = x3 * y3 * (x1 - x2) + x2 * y2 * (x3 - x1) + x1 * y1 * (x2 - x3); const c1 = (a11 * y1 + a12 * y2 + a13 * y3) / alpha; const c2 = (a21 * y1 + a22 * y2 + a23 * y3) / alpha; const c3 = Math.max((a31 * y1 + a32 * y2 + a33 * y3) / alpha, 0); /* apply tone map to image */ for (let i = 0; i < imageData.data.length; i += 4) { const r_pq = imageData.data[i] / 255; const g_pq = imageData.data[i + 1] / 255; const b_pq = imageData.data[i + 2] / 255; /* invert PQ */ const r = 10000 * pqEOTF(r_pq); const g = 10000 * pqEOTF(g_pq); const b = 10000 * pqEOTF(b_pq); /* tone map */ let rt = (c1 + c2 * r)/(1 + c3 * r); let gt = (c1 + c2 * g)/(1 + c3 * g); let bt = (c1 + c2 * b)/(1 + c3 * b); /* color space convert */ [rt, gt, bt] = P3D65ToRec709(rt, gt, bt); /* gamma */ const rp = Math.pow(rt / sdrL, 1/2.4); const gp = Math.pow(gt / sdrL, 1/2.4); const bp = Math.pow(bt / sdrL, 1/2.4); /* put pixel back */ imageData.data[i] = 255*rp; imageData.data[i + 1] = 255*gp; imageData.data[i + 2] = 255*bp; } } function st209410ToneMapDefault(imageData) { const sdrL = 100; /* Smith: 10-bit: 0.005 nits is 10bit CV 15, 10 nits is 10bit CV 307, 1000 nits is 10bit CV 769 */ /* 8-bit: 0.005 is 4, 10.0 is 76 and 1000.0 is 192 */ let minPQ = 15/1023; let maxPQ = 769/1023; let avgPQ = 307/1023; document.getElementById("defaultMinPQ").innerText = minPQ.toFixed(3); document.getElementById("defaultAvgPQ").innerText = avgPQ.toFixed(3); document.getElementById("defaultMaxPQ").innerText = maxPQ.toFixed(3); // console.log(`min: ${minPQ}, avg: ${avgPQ}, max: ${maxPQ}`); /* constants */ const x1 = 10000 * pqEOTF(minPQ); const x2 = 10000 * pqEOTF(avgPQ); const x3 = 10000 * pqEOTF(maxPQ); // console.log(`x1: ${x1}, x2: ${x2}, x3: ${x3}`); const y1 = 0.1; const y3 = sdrL; const y2 = Math.sqrt(x2 * Math.sqrt(y3 * y1)); const a11 = x2 * x3 * (y2 - y3); const a12 = x1 * x3 * (y3 - y1); const a13 = x1 * x2 * (y1 - y2); const a21 = x3 * y3 - x2 * y2; const a22 = x1 * y1 - x3 * y3; const a23 = x2 * y2 - x1 * y1; const a31 = x3 - x2; const a32 = x1 - x3; const a33 = x2 - x1; const alpha = x3 * y3 * (x1 - x2) + x2 * y2 * (x3 - x1) + x1 * y1 * (x2 - x3); const c1 = (a11 * y1 + a12 * y2 + a13 * y3) / alpha; const c2 = (a21 * y1 + a22 * y2 + a23 * y3) / alpha; const c3 = Math.max((a31 * y1 + a32 * y2 + a33 * y3) / alpha, 0); /* apply tone map to image */ for (let i = 0; i < imageData.data.length; i += 4) { const r_pq = imageData.data[i] / 255; const g_pq = imageData.data[i + 1] / 255; const b_pq = imageData.data[i + 2] / 255; /* invert PQ */ const r = 10000 * pqEOTF(r_pq); const g = 10000 * pqEOTF(g_pq); const b = 10000 * pqEOTF(b_pq); /* tone map */ let rt = (c1 + c2 * r)/(1 + c3 * r); let gt = (c1 + c2 * g)/(1 + c3 * g); let bt = (c1 + c2 * b)/(1 + c3 * b); /* color space convert */ [rt, gt, bt] = P3D65ToRec709(rt, gt, bt); /* gamma */ const rp = Math.pow(rt / sdrL, 1/2.4); const gp = Math.pow(gt / sdrL, 1/2.4); const bp = Math.pow(bt / sdrL, 1/2.4); /* put pixel back */ imageData.data[i] = 255*rp; imageData.data[i + 1] = 255*gp; imageData.data[i + 2] = 255*bp; } } function colorSpaceConversionOnly(imageData) { for (let i = 0; i < imageData.data.length; i += 4) { const rp = imageData.data[i] / 255; const gp = imageData.data[i + 1] / 255; const bp = imageData.data[i + 2] / 255; /* invert gamma */ let r = Math.pow(rp, 2.4); let g = Math.pow(gp, 2.4); let b = Math.pow(bp, 2.4); /* color space convert */ [r, g, b] = P3D65ToRec709(r, g, b); /* put pixel back */ imageData.data[i] = 255 * Math.pow(r, 1/2.4); imageData.data[i + 1] = 255 * Math.pow(g, 1/2.4); imageData.data[i + 2] = 255 * Math.pow(b, 1/2.4); } } function toneMapIntoCanvas(toneFunc, canvas, pqImage) { let ctx = canvas.getContext('2d'); canvas.height = pqImage.height; canvas.width = pqImage.width; ctx.drawImage(pqImage, 0, 0); let imageData = ctx.getImageData(0, 0, pqImage.width, pqImage.height); toneFunc(imageData); ctx.putImageData(imageData, 0, 0); } document.addEventListener("DOMContentLoaded", () => { let pqImage = new Image(); pqImage.onload = function() { let canvas1 = document.getElementById("canvas-1"); let canvas2 = document.getElementById("canvas-2"); let canvas3 = document.getElementById("canvas-3"); let canvas4 = document.getElementById("canvas-4"); let canvas5 = document.getElementById("canvas-5"); toneMapIntoCanvas(ituToneMap, canvas2, pqImage); toneMapIntoCanvas(st209410ToneMap, canvas1, pqImage); toneMapIntoCanvas(colorSpaceConversionOnly, canvas3, pqImage); toneMapIntoCanvas(st209410ToneMapDefault, canvas4, pqImage); toneMapIntoCanvas(itu2408ToneMap, canvas5, pqImage); }; pqImage.src = "Meridian_UHD4k5994p_HDR_P3PQ_02000.scaled.png"; });