[argyllcms] Re: Calibrate to BT.2100 PQ curve?

  • From: Niklas Haas <argyll@xxxxxxxxx>
  • To: Roger Breton <graxx@xxxxxxxxxxxx>
  • Date: Fri, 23 Feb 2018 18:20:29 +0100

Hi Roger,

PQ maps 1.0 to an absolute value of 10,000 cd/m^2. (Of course, this is a
mostly theoretical limit. In practice, PQ monitors are not expected to
achieve more than 500-1000 cd/m^2)

That also raises a question: do we want to calibrate to "raw" PQ (i.e.
anything above the highest supported brightness level gets hard-clipped
to 1.0), or do we want to include some sort of "knee" function to
automatically roll-off the response near the display's peak brightness?

Anyway, I gave an implementation a try. It sort of seems to work (I can
run a successful calibration and the resulting CLUT EOTF cancels out the
PQ OETF - I can view a native PQ file and it looks normal on my screen),
however I would have expected it to hard-clip above a certain value
because I told it to calibrate to 10000 cd/m^2 peak, rather than "as
measured", so maybe ArgyllCMS is already including some sort of knee
function to prevent clipping when -b exceeds the display's capabilities?

Regards,
Niklas Haas

On Fri, 23 Feb 2018 12:05:51 -0500, "Roger Breton" <graxx@xxxxxxxxxxxx> wrote:

Hey Niklas,

Not familiar with BT.2100 PQ but what is the maximum PQ value for Luminance? 
160? 180? 200? More?

Best / Roger

-----Original Message-----
From: argyllcms-bounce@xxxxxxxxxxxxx [mailto:argyllcms-bounce@xxxxxxxxxxxxx] ;
On Behalf Of Niklas Haas
Sent: Friday, February 23, 2018 10:31 AM
To: argyllcms@xxxxxxxxxxxxx
Subject: [argyllcms] Calibrate to BT.2100 PQ curve?

Hey,

Long time user of ArgyllCMS here. Nothing else comes close, so thanks for the 
great tools!

Given the prominence of the BT.2100 PQ (Perceptual Quantizer) standard in HDR 
displays and mastering, is it planned to add PQ as a supported gamma type for 
`dispcal`?

In principle, the only major difference to the existing curves like `-gs` or 
`-g709` is that the response is compared against an absolute scale, rather 
than a relative scale. That is, a PQ value of 0.5 (=512 on a 10-bit output) 
must always be exactly 92 cd/m² - regardless of the display's measured white 
point.

Thanks,
Niklas Haas

P.s. I'm not an expert on ICC profiles - but I wonder how HDR ICC profile 
generation would be done. Given the fact that SDR ICC profiles are normalized 
such that XYZ Y=1 maps to the display's maximum brightness, it probably 
wouldn't be wise to do the same for HDR displays. Instead, I'm thinking we 
could have Y=1 map to the “nominal”
white (i.e. a signal level of 520 for an ideal PQ display), and use values 
above 1 to represent super-highlights. Would this also be possible with 
ArgyllCMS?


diff --git a/spectro/dispcal.c b/spectro/dispcal.c
index a4798af..f9fcec2 100755
--- a/spectro/dispcal.c
+++ b/spectro/dispcal.c
@@ -211,7 +211,8 @@ typedef enum {
        gt_Lab       = 1,       /* The L* curve */
        gt_sRGB      = 2,       /* The sRGB curve */
        gt_Rec709    = 3,       /* REC 709 video standard */
-       gt_SMPTE240M = 4        /* SMTPE 240M video standard */
+       gt_SMPTE240M = 4,       /* SMTPE 240M video standard */
+       gt_PQ        = 5        /* The PQ curve */
 } gammatype;
 
 /* Context for calibration solution */
@@ -266,6 +267,14 @@ typedef struct {
 /* - - - - - - - - - - - - - - - - - - - */
 /* Ideal target curve definitions */
 
+/* Values taken from ITU-R Rec BT.2100 */
+static const double
+       pq_m1 = 2610.0 / 16384.0,
+       pq_m2 = 2523.0 / 4096.0 * 128.0,
+       pq_c1 = 3424.0 / 4096.0,
+       pq_c2 = 2413.0 / 128.0,
+       pq_c3 = 2392.0 / 128.0;
+
 /* Convert ideal device (0..1) to target Y value (0..1) */
 static double dev2Y(calx *x, double egamma, double vv) {
 
@@ -299,6 +308,12 @@ static double dev2Y(calx *x, double egamma, double vv) {
                                vv = pow((0.1115 + vv)/1.1115, 1.0/0.45);
                        break;
                } 
+               case gt_PQ: {
+                       vv = pow(vv, 1.0/pq_m2);
+                       vv = fmax(vv - pq_c1, 0) / (pq_c2 - pq_c3 * vv);
+                       vv = pow(vv, 1.0/pq_m1);
+                       break;
+               }
                default:
                        error("Unknown gamma type");
        }
@@ -338,6 +353,12 @@ static double Y2dev(calx *x, double egamma, double vv) {
                                vv = pow(vv, 0.45) * 1.1115 - 0.1115;
                        break;
                } 
+               case gt_PQ: {
+                       vv = pow(vv, pq_m1);
+                       vv = (pq_c1 + pq_c2 * vv) / (1.0 + pq_c3 * vv);
+                       vv = pow(vv, pq_m2);
+                       break;
+                }
                default:
                        error("Unknown gamma type");
        }
@@ -1616,6 +1637,7 @@ void usage(int flag, char *diag, ...) {
        fprintf(stderr,"                      Use \"-g709\" for REC 709 curve 
(should use -a as well!)\n");
        fprintf(stderr,"                      Use \"-g240\" for SMPTE 240M 
curve (should use -a as well!)\n");
        fprintf(stderr,"                      Use \"-G2.4 -f0\" for 
BT.1886\n"); 
+       fprintf(stderr,"                      Use \"-gpq -b10000\" for Dobly 
PQ\n");
        fprintf(stderr," -G gamma             Set the target response curve 
actual technical gamma\n");
        fprintf(stderr," -f [degree]          Amount of black level accounted 
for with output offset (default all output offset)\n");
        fprintf(stderr," -a ambient           Use viewing condition adjustment 
for ambient in Lux\n");
@@ -2169,6 +2191,8 @@ int main(int argc, char *argv[]) {
                                        x.gammat = gt_Rec709;
                                else if (strcmp(na, "240") == 0)
                                        x.gammat = gt_SMPTE240M;
+                               else if (strcmp(na, "pq") == 0 || strcmp(na, 
"PQ") == 0)
+                                       x.gammat = gt_PQ;
                                else {
                                        gamma = atof(na);
                                        if (gamma <= 0.0 || gamma > 10.0) 
usage(0,"-g parameter %f out of range",gamma);
@@ -2251,6 +2275,13 @@ int main(int argc, char *argv[]) {
                        break;
        }
 
+        /* Force 10,000 cd/m^2 for PQ curve */
+       if (x.gammat == gt_PQ) {
+               if (tbright != 10000)
+                       warning("Due to -gpq, -b will be set to 10000");
+               tbright = 10000;
+       }
+
        if (bkhack && bkbright > 0.0) {
                error("Can't use -b black point hack and set target black 
brightness");
        }
@@ -2724,6 +2755,8 @@ int main(int argc, char *argv[]) {
                        x.gammat = gt_Rec709;
                else if (strcmp(icg->t[0].kdata[fi], "SMPTE240M") == 0)
                        x.gammat = gt_SMPTE240M;
+               else if (strcmp(icg->t[0].kdata[fi], "PQ") == 0)
+                       x.gammat = gt_PQ;
                else {
                        x.gammat = 0;
                        gamma = atof(icg->t[0].kdata[fi]);
@@ -3075,6 +3108,9 @@ int main(int argc, char *argv[]) {
                        case gt_SMPTE240M:
                                printf("Target gamma = SMPTE 240M curve\n");
                                break;
+                       case gt_PQ:
+                               printf("Target gamma = PQ curve\n");
+                               break;
                        default:
                                error("Unknown gamma type");
                }
@@ -4407,7 +4443,8 @@ int main(int argc, char *argv[]) {
                                break;
 
                        case gt_Rec709:
-                       case gt_SMPTE240M:      /* Television studio conditions 
*/
+                       case gt_SMPTE240M:
+                       case gt_PQ:     /* Television studio conditions */
                                x.svc->set_view(x.svc, vc_none,
                                        x.nwh,                                  
/* Display normalised white point */
                                        0.2 * 1000.0/3.1415,    /* Adapting 
luminence, 20% of 1000 lux in cd/m^2 */
@@ -5299,6 +5336,9 @@ int main(int argc, char *argv[]) {
                        case gt_SMPTE240M:
                                strcpy(buf,"SMPTE240M");
                                break;
+                       case gt_PQ:
+                               strcpy(buf,"PQ");
+                               break;
                        default:
                                error("Unknown gamma type");
                }

Other related posts: