snibgo's ImageMagick pages

Jzazbz colorspace

This has been promoted as a more "modern" version of CIE Lab, especially for high dynamic ranges.

For ImageMagick, I propose two new colorspaces: Jzazbz and its polar version JzCzhz. They should be used with HDRI.

References

Introduction

Jzazbz is a a colour space designed for perceptul uniformity in high dynamic range (HDR) and wide colour gamut (WCG) applications. Conceptually it is similar to CIE L*a*b*, but has claimed improvements:

Note that uniformity and linearity trade-off against each other. There is no perfect colorspace. Parameters in this colour space have been optimised to give the best scores in certain tests.

Each "z" is a subscript, hence the name is properly Jzazbz, but is often written as Jzazbz. There are three channels: Jz (lightness), az (redness-greenness) and bz (yellowness-blueness).

Unlike some traditional ImageMagick colorspaces, Jzazbz determines absolute luminance, not merely relative luminance. A relative luminance is with respect to the maximum luminance an input device (such as a scanner or camera) can record, or the maximum luminance an output device (such as a screen) can display. So a relative luminance of 100% is the maximum a camera can record or a screen can display. But Jzazbz is designed to accurately record absolute luminances. For example, 100% in the Jz channel corresponds to a luminance of 10000 cd/m2, candelas per square metre.

EOTF PQ perceptual quantizer

For the polar coordinate version JzCzhz, chroma and hue are defined in the usual way:

Cz = sqrt(az2+bz2)
hz = atan2(bz,az)

Code changes

The two functions that transform between XYZ and Jzazbz are:

cgrep /p0 /i%IM7SRC%\MagickCore\colorspace.c /ojz_func.lis /sJzazbz_b /tConvertXYZToJzCzhz /d
#define Jzazbz_b 1.15
#define Jzazbz_g 0.66
#define Jzazbz_c1 (3424/4096.0)
#define Jzazbz_c2 (2413/128.0)
#define Jzazbz_c3 (2392/128.0)
#define Jzazbz_n (2610/16384.0)
#define Jzazbz_p (1.7*2523/32.0)
#define Jzazbz_d (-0.56)
#define Jzazbz_d0 (1.6295499532821566e-11)

static void inline ConvertXYZToJzazbz(const double X,const double Y,
  const double Z,double *Jz,double *az,double *bz,
  const double peakLum, const MagickBooleanType debug)
{
  double
    Xp,
    Yp,
    Zp,
    L,
    M,
    S,
    Lp,
    Mp,
    Sp,
    Iz,
    tmp;

  Xp = (Jzazbz_b*X - (Jzazbz_b-1)*Z);
  Yp = (Jzazbz_g*Y - (Jzazbz_g-1)*X);
  Zp = Z;

  L = 0.41478972*Xp + 0.579999*Yp + 0.0146480*Zp;
  M = -0.2015100*Xp + 1.120649*Yp + 0.0531008*Zp;
  S = -0.0166008*Xp + 0.264800*Yp + 0.6684799*Zp;

  tmp=pow(L/peakLum,Jzazbz_n);
  Lp=pow((Jzazbz_c1+Jzazbz_c2*tmp)/(1+Jzazbz_c3*tmp),Jzazbz_p);

  tmp=pow(M/peakLum,Jzazbz_n);
  Mp=pow((Jzazbz_c1+Jzazbz_c2*tmp)/(1+Jzazbz_c3*tmp),Jzazbz_p);

  tmp=pow(S/peakLum,Jzazbz_n);
  Sp=pow((Jzazbz_c1+Jzazbz_c2*tmp)/(1+Jzazbz_c3*tmp),Jzazbz_p);

  Iz  = 0.5*Lp + 0.5*Mp;
  *az = 3.52400*Lp  - 4.066708*Mp + 0.542708*Sp + 0.5;
  *bz = 0.199076*Lp + 1.096799*Mp - 1.295875*Sp + 0.5;

  *Jz = (1+Jzazbz_d)*Iz/(1+Jzazbz_d*Iz) - Jzazbz_d0;

  if (debug) {
    fprintf (stderr, "XYZ to Jzazbz:\n");
    fprintf (stderr, "  X=%g Y=%g Z=%g\n", X, Y, Z);
    fprintf (stderr, "  Xp=%g Yp=%g Zp=%g\n", Xp, Yp, Zp);
    fprintf (stderr, "  L=%g M=%g S=%g\n", L, M, S);
    fprintf (stderr, "  Lp=%g Mp=%g Sp=%g\n", Lp, Mp, Sp);
    fprintf (stderr, "  Jz=%g az=%g bz=%g\n", *Jz, *az, *bz);
  }
}

static void inline ConvertJzazbzToXYZ(const double Jz,const double az,
  const double bz,double *X,double *Y,double *Z,
  const double peakLum, const MagickBooleanType debug)
{
  double
    azz,
    bzz,
    Xp,
    Yp,
    Zp,
    L,
    M,
    S,
    Lp,
    Mp,
    Sp,
    Iz,
    tmp;

  tmp = Jz + Jzazbz_d0;
  Iz = tmp / (1.0 + Jzazbz_d - Jzazbz_d*tmp);

  azz = az - 0.5;
  bzz = bz - 0.5;

  Lp = Iz + 0.138605043271539*azz  + 0.0580473161561189*bzz;
  Mp = Iz - 0.138605043271539*azz  - 0.0580473161561189*bzz;
  Sp = Iz - 0.0960192420263189*azz - 0.811891896056039*bzz;

  tmp = pow (Lp, 1/Jzazbz_p);
  L = peakLum * pow((Jzazbz_c1 - tmp)/(Jzazbz_c3*tmp-Jzazbz_c2),1/Jzazbz_n);

  tmp = pow (Mp, 1/Jzazbz_p);
  M = peakLum * pow((Jzazbz_c1 - tmp)/(Jzazbz_c3*tmp-Jzazbz_c2),1/Jzazbz_n);

  tmp = pow (Sp, 1/Jzazbz_p);
  S = peakLum * pow((Jzazbz_c1 - tmp)/(Jzazbz_c3*tmp-Jzazbz_c2),1/Jzazbz_n);

  Xp =  1.92422643578761*L   - 1.00479231259537*M  + 0.037651404030618*S;
  Yp =  0.350316762094999*L  + 0.726481193931655*M - 0.065384422948085*S;
  Zp = -0.0909828109828476*L - 0.312728290523074*M + 1.52276656130526*S;

  *X = (Xp + (Jzazbz_b - 1) * Zp ) / Jzazbz_b;
  *Y = (Yp + (Jzazbz_g - 1) * *X ) / Jzazbz_g;
  *Z = Zp;

  if (debug) {
    fprintf (stderr, "Jzazbz to XYZ:\n");
    fprintf (stderr, "  Jz=%g az=%g bz=%g\n", Jz, az, bz);
    fprintf (stderr, "  Lp=%g Mp=%g Sp=%g\n", Lp, Mp, Sp);
    fprintf (stderr, "  L=%g M=%g S=%g\n", L, M, S);
    fprintf (stderr, "  Xp=%g Yp=%g Zp=%g\n", Xp, Yp, Zp);
    fprintf (stderr, "  X=%g Y=%g Z=%g\n", *X, *Y, *Z);
  }
}

Inputs and outputs to these functions are in the nominal range 0.0 to 1.0.

Numerical values

We investigate the numerical values when converting from sRGB to Jzazbz. For convenience, we use an environment variable for the format:

set FMT=^
Jz: min=%%[fx:minima.r] ^
mean=%%[fx:mean.r] ^
max=%%[fx:maxima.r] ^
SD=%%[fx:standard_deviation.r]\n^
az: min=%%[fx:minima.g] ^
mean=%%[fx:mean.g] ^
max=%%[fx:maxima.g] ^
SD=%%[fx:standard_deviation.g]\n^
bz: min=%%[fx:minima.b] ^
mean=%%[fx:mean.b] ^
max=%%[fx:maxima.b] ^
SD=%%[fx:standard_deviation.b]\n

Convert the usual test image to Jzazbz, and report the values:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Jzazbz ^
  -format "%FMT%" ^
  info: 
Jz: min=0.000621512 mean=0.00639987 max=0.0158055 SD=0.00287985
az: min=0.497448 mean=0.50176 max=0.506396 SD=0.0012926
bz: min=0.493823 mean=0.499727 max=0.504816 SD=0.00172271

Note that the values in the Jz channel are all close to 0.0, and the values in the az and bz channels are close to 0.5.

Repeat the experiment, using a full range of sRGB colours:

%IM7DEV%magick ^
  hald:12 ^
  -set colorspace sRGB ^
  -colorspace Jzazbz ^
  -format "%FMT%" ^
  info: 
Jz: min=0 mean=0.00798432 max=0.01758 SD=0.00352318
az: min=0.483752 mean=0.500026 max=0.517215 SD=0.00725392
bz: min=0.475052 mean=0.500252 max=0.5208 SD=0.00958028

This has stretched the range of output values, but only slightly.

As before, but setting the peak luminance to 1:

%IM7DEV%magick ^
  toes.png ^
  -set JzazbzPeakLum 1 ^
  -colorspace Jzazbz ^
  -format "%FMT%" ^
  info: 
Jz: min=0.166079 mean=0.525886 max=0.920508 SD=0.134593
az: min=0.453402 mean=0.528942 max=0.619876 SD=0.0181065
bz: min=0.348284 mean=0.493328 max=0.610654 SD=0.0344468
%IM7DEV%magick ^
  hald:12 ^
  -set colorspace sRGB ^
  -set JzazbzPeakLum 1 ^
  -colorspace Jzazbz ^
  -format "%FMT%" ^
  info: 
Jz: min=0 mean=0.594479 max=0.988608 SD=0.161771
az: min=0.320729 mean=0.498773 max=0.725485 SD=0.102605
bz: min=0.176901 mean=0.509051 max=0.722631 SD=0.140453

Note that not all tuples of Jzazbz are visible colours.

We repeat, for JzCzhz:

set FMT=^
Jz: min=%%[fx:minima.r] ^
mean=%%[fx:mean.r] ^
max=%%[fx:maxima.r] ^
SD=%%[fx:standard_deviation.r]\n^
Cz: min=%%[fx:minima.g] ^
mean=%%[fx:mean.g] ^
max=%%[fx:maxima.g] ^
SD=%%[fx:standard_deviation.g]\n^
hz: min=%%[fx:minima.b] ^
mean=%%[fx:mean.b] ^
max=%%[fx:maxima.b] ^
SD=%%[fx:standard_deviation.b]\n
%IM7DEV%magick ^
  toes.png ^
  -colorspace JzCzhz ^
  -format "%FMT%" ^
  info: 
Jz: min=0.000621512 mean=0.00639987 max=0.0158055 SD=0.00287985
Cz: min=1.11506e-05 mean=0.00257249 max=0.00776567 SD=0.00109301
hz: min=1.15599e-06 mean=0.470807 max=0.999992 SD=0.38524

Note that the values in the Jz and Cz channels are all close to 0.0, but the hz (hue) channel stretches almost from 0.0 to 1.0.

Repeat the experiment, using a full range of sRGB colours:

%IM7DEV%magick ^
  hald:12 ^
  -set colorspace sRGB ^
  -colorspace JzCzhz ^
  -format "%FMT%" ^
  info: 
Jz: min=0 mean=0.00798432 max=0.01758 SD=0.00352318
Cz: min=0 mean=0.0108804 max=0.0249751 SD=0.0051071
hz: min=0 mean=0.495311 max=1 SD=0.281704

This has stretched the range of Jz and Cz output values, but only slightly.

As before, but setting the peak luminance to 1:

%IM7DEV%magick ^
  toes.png ^
  -set JzazbzPeakLum 1 ^
  -colorspace JzCzhz ^
  -format "%FMT%" ^
  info: 
Jz: min=0.166079 mean=0.525886 max=0.920508 SD=0.134593
Cz: min=0.000111764 mean=0.045205 max=0.18651 SD=0.0187905
hz: min=6.79336e-06 mean=0.472668 max=0.999997 SD=0.384207
%IM7DEV%magick ^
  hald:12 ^
  -set colorspace sRGB ^
  -set JzazbzPeakLum 1 ^
  -colorspace JzCzhz ^
  -format "%FMT%" ^
  info: 
Jz: min=0 mean=0.594479 max=0.988608 SD=0.161771
Cz: min=0 mean=0.161171 max=0.339616 SD=0.0660455
hz: min=0 mean=0.4947 max=1 SD=0.274152

Hue angles

What are the hue angles, on a scale of 0.0 to 360.0, of sRGB red, yellow, green, cyan, blue and magenta?

%IM7DEV%magick ^
  xc:#f00 xc:#ff0 xc:#0f0 xc:#0ff xc:#00f xc:#f0f ^
  -colorspace JzCzhz ^
  -format "%%[fx:mean.b*360] " ^
  info: 
35.2201 319.445 267.332 204.938 134.444 100.148 

As expected, these are roughly 60° apart.

Round-trip tests

We test the round-trip sRGB→Jzazbz→sRGB. First, with the usual toes.png image:

%IM7DEV%magick ^
  toes.png ^
  ( +clone ^
    -colorspace Jzazbz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
3.25111e-08

Next we test with hald:16, which contains all 8-bit/channel colours:

%IM7DEV%magick ^
  hald:16 ^
  -set colorspace sRGB ^
  ( +clone ^
    -colorspace Jzazbz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
1.49843e-07

As previous, but setting the peak luminance to 1:

%IM7DEV%magick ^
  hald:16 ^
  -set colorspace sRGB ^
  ( +clone ^
    -set JzazbzPeakLum 1 ^
    -colorspace Jzazbz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
1.49843e-07

In all cases, the RMSE distortion (on a scale of 0.0 to 1.0) is very small, so the round-trips are successful.

We repeat the three tests, using the JzCzhz colour space:

%IM7DEV%magick ^
  toes.png ^
  ( +clone ^
    -colorspace JzCzhz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
3.25111e-08

Next we test with hald:16, which contains all 8-bit/channel colours:

%IM7DEV%magick ^
  hald:16 ^
  -set colorspace sRGB ^
  ( +clone ^
    -colorspace JzCzhz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
1.49843e-07

As previous, but setting the peak luminance to 1:

%IM7DEV%magick ^
  hald:16 ^
  -set colorspace sRGB ^
  ( +clone ^
    -set JzazbzPeakLum 1 ^
    -colorspace JzCzhz ^
    -colorspace sRGB ) ^
  -metric RMSE -format %%[distortion] -compare ^
  info: 
1.49843e-07

Again, the RMSE distortions (on a scale of 0.0 to 1.0) are very small, so the round-trips are successful.

Applications

For a sample input we will use an sRGB image:

toes.png

toes.pngjpg

Grayscale

We make a grayscale image:

In HCL colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace HCL ^
  -channel 1 -evaluate Set 0 +channel ^
  -colorspace sRGB ^
  jz_gr0.jpg
jz_gr0.jpg

In L*a*b* colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Lab ^
  -channel 1,2 -evaluate Set 50%% +channel ^
  -colorspace sRGB ^
  jz_gr1.jpg
jz_gr1.jpg

In Jzazbz colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Jzazbz ^
  -channel 1,2 -evaluate Set 50%% +channel ^
  -colorspace sRGB ^
  jz_gr2.jpg
jz_gr2.jpg

In JzCzhz colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace JzCzhz ^
  -channel 1 -evaluate Set 0 +channel ^
  -colorspace sRGB ^
  jz_gr3.jpg
jz_gr3.jpg

Saturation

We increase saturation, multiplying by a factor K to increase saturation by 50%:

set K=1.5

In JzCzhz colorspace, the task is simple as we have a "saturation" channel.

In Lab and Jzazbz the a and b channels are neutral at 0.5 on a scale from 0.0 to 1.0, so to change saturation we move values away from 0.5, so we transform by subtracting 0.5, multiplying by K, then re-adding 0.5.

v' = (v-0.5)*K + 0.5
   = v*K + (0.5 - 0.5*K)

In HCL colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace HCL ^
  -channel 1 ^
  -evaluate Multiply %K% ^
  +channel ^
  -colorspace sRGB ^
  jz_incsat0.jpg
jz_incsat0.jpg

In L*a*b* colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Lab ^
  -channel 1,2 ^
  -function Polynomial %K%,%%[fx:0.5-0.5*%K%] ^
  +channel ^
  -colorspace sRGB ^
  jz_incsat1.jpg
jz_incsat1.jpg

In Jzazbz colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Jzazbz ^
  -channel 1,2 ^
  -function Polynomial %K%,%%[fx:0.5-0.5*%K%] ^
  +channel ^
  -colorspace sRGB ^
  jz_incsat2.jpg
jz_incsat2.jpg

In JzCzhz colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace JzCzhz ^
  -channel 1 ^
  -evaluate Multiply %K% ^
  +channel ^
  -colorspace sRGB ^
  jz_incsat2.jpg
jz_incsat2.jpg

These colorspaces are not suitable for setting maximum saturation, because the maximum valid values of Cz depend on both Jz and hz. This is one price we pay for having all colours with the same Cz having the same perceived saturation.

Also remember that "maximum saturation" is usually a display-referred concept: the maximum saturation a particular display can show. In the real world, "maximum saturation" means monochromatic light, eg from a laser, and it falls on the horseshoe edge of a CIE xy chromaticity diagram. In any RGB colorspace, the purest colours (on some edge of the triangle) may fall inside or outside the horseshoe, depending on the primaries. But the triangle edges cannot coincide with the horseshoe edges.

Clustering

See K-clustering.

In sRGB colorspace:

%IM7DEV%magick ^
  toes.png ^
  -process kcluster ^
  jz_kclus1.png
jz_kclus1.pngjpg

In Jzazbz colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Jzazbz ^
  -process kcluster ^
  -colorspace sRGB ^
  jz_kclus2.png
jz_kclus2.pngjpg

Oops. What happened there? The process kcluster calls "-colors", which doesn't like colorspaces other than sRGB.

Possible cures:

Pretend we are in sRGB colorspace:

%IM7DEV%magick ^
  toes.png ^
  -colorspace Jzazbz ^
  -set colorspace sRGB ^
  -process 'kcluster' ^
  -set colorspace Jzazbz ^
  -colorspace sRGB ^
  jz_kclus3.png
jz_kclus3.pngjpg

In Jzazbz colorspace, but with PeakLum=1:

%IM7DEV%magick ^
  toes.png ^
  -set JzazbzPeakLum 1 ^
  -colorspace Jzazbz ^
  -process kcluster ^
  -colorspace sRGB ^
  jz_kclus4.png
jz_kclus4.pngjpg

With PeakLum=1, and pretend we are in sRGB colorspace:

%IM7DEV%magick ^
  toes.png ^
  -set JzazbzPeakLum 1 ^
  -colorspace Jzazbz ^
  -set colorspace sRGB ^
  -process kcluster ^
  -set colorspace Jzazbz ^
  -colorspace sRGB ^
  jz_kclus5.png
jz_kclus5.pngjpg

All images on this page were created by the commands shown, using:

%IM7DEV%magick -version
Version: ImageMagick 7.0.8-64 Q32 x86_64 2019-12-18 https://imagemagick.org
Copyright: © 1999-2019 ImageMagick Studio LLC
License: https://imagemagick.org/script/license.php
Features: Cipher DPC HDRI Modules OpenMP(4.5) 
Delegates (built-in): bzlib cairo fftw fontconfig fpx freetype jbig jng jpeg lcms ltdl lzma pangocairo png rsvg tiff webp wmf x xml zlib

Source file for this web page is jzazbz.h1. To re-create this web page, execute procH1 jzazbz.


This page, including the images, is my copyright. Anyone is permitted to use or adapt any of the code, scripts or images for any purpose, including commercial use.

Anyone is permitted to re-publish this page, but only for non-commercial use.

Anyone is permitted to link to this page, including for commercial use.


Page version v1.0 24-September-2019.

Page created 17-Jan-2020 04:00:31.

Copyright © 2020 Alan Gibson.