coloria-dev / coloria

:rainbow: Tools for color research
385 stars 32 forks source link

range of JzazBz coordinates #41

Closed eonadler closed 5 years ago

eonadler commented 5 years ago

We have encountered the following issue, which has been reproduced using a few independent RGB --> JzAzBz implementations (including colorio and https://github.com/quag/JzAzBz/blob/master/python3/srgb255ToJzAzBz.py) ---

The JzAzBz paper (https://doi.org/10.1364/OE.25.015131) seems to suggest that the coordinates should span roughly Jz in (0,1), Az in (-0.5, 0.5), Bz in (-0.5,0.5). However, when we explicitly map all RGB tuples in (0,255) x (0,255) x (0,255) to JzAzBz as follows, we find that (Jz, Az, Bz) span (0,0.167), (-0.09,1.09), (-0.156,0.115).

So we're wondering if the original paper renormalized their coordinates, or if there's a mistake on our end.

Extremely simplified code that demonstrates the basic issue:

import numpy as np
import colorio

colorspace = colorio.SrgbLinear()
colorspace2 = colorio.JzAzBz()

jzazbz_test = np.zeros((256,256,256,3))
for i in range(0,255):
    for j in range(0,255):
        for k in range(0,255):
            jzazbz_test[i][j][k] = colorspace2.from_xyz100(colorspace.to_xyz100(colorspace.from_srgb1((i/255.,j/255.,k/255.))))

print('min Jz={}'.format(np.min(jzazbz_test[:,:,:,0])))
print('max Jz={}'.format(np.max(jzazbz_test[:,:,:,0])))

print('min Az={}'.format(np.min(jzazbz_test[:,:,:,1])))
print('max Az={}'.format(np.max(jzazbz_test[:,:,:,1])))

print('min Bz={}'.format(np.min(jzazbz_test[:,:,:,2])))
print('max Bz={}'.format(np.max(jzazbz_test[:,:,:,2])))
nschloe commented 5 years ago

Without getting to the bottom of the matter, let me first suggest to vectorize the code. Python loops are awefully slow, so you never want to use them. This

import numpy as np
import colorio

colorspace = colorio.SrgbLinear()
colorspace2 = colorio.JzAzBz()

X = np.linspace(0.0, 1.0, 256)
x, y, z = np.meshgrid(X, X, X)
pts = np.array([x, y, z])

jzazbz_test = colorspace2.from_xyz100(colorspace.to_xyz100(colorspace.from_srgb1(pts)))

print("min Jz={}".format(np.min(jzazbz_test[0])))
print("max Jz={}".format(np.max(jzazbz_test[0])))

print("min Az={}".format(np.min(jzazbz_test[1])))
print("max Az={}".format(np.max(jzazbz_test[1])))

print("min Bz={}".format(np.min(jzazbz_test[2])))
print("max Bz={}".format(np.max(jzazbz_test[2])))

is about 70 times faster.

Next, a picture of the SRGB gamut, created with

import colorio

colorio.show_srgb_gamut(colorio.JzAzBz(), "out.vtu", n=100) 

jz

Okay, this conincides with what you've measured.

In the article, they're referring to the Rec.2020 cube which is larger than sRGB, so there's one difference. The difference in the scales however seems too big to explain that. Perhaps I could add a function that returns the Rec.2020 gamut in any color space to confirm.

It'd be awesome if you could check the values of colorio against their MATLAB :frowning_face: reference implementation at https://figshare.com/articles/JzAzBz_m/5016299.

nschloe commented 5 years ago

Okay now, here's the Rec.2020 (HDR) gamut. Doesn't look right either, so need to confirm with the MATLAB code.

hdr

jrus commented 5 years ago

The “XYZ” coordinates they are using are counted in nits, and range from 0 to 10000. It’s not clear if the best way to use the color space is to scale white to 10000, or instead to leave the maximum at whatever the actual luminance of the display is.

jrus commented 5 years ago

If you want to compare against another implementation I believe https://observablehq.com/@jrus/jzazbz is correct w/r/t the paper.

nschloe commented 5 years ago

I've just checked with the Ebner-Fairchild and Hung-Berns plots of JzAzBz, and they look alright in the current implementation. The Rec2020 plot looks wrong.

When changing to nits, Ebner-Fairchild and Hung-Berns are out of whack while the SRGB (Rec. 709) blob looks like what they have as Rec 2020.

Something is wrong.

I've tried contacting the authors over a year ago, but only got back to M. Safdar who referred me to M.R. Luo who never replied.

jrus commented 5 years ago

I don’t know what specifically you are plotting or what “look alright” or “out of whack” mean.

I would recommend you try scaling white to 10,000 before converting to Jzazbz in every one of your desired plots and see what happens.

nschloe commented 5 years ago

Okay, I've added some images to make the situation clearer:

On master: Ebner-Fairchild, Hung-Berns, and Munsell data seem okay.

With the suggested change, everything seems wrong, but the gamut resembles more closely what's given in the article.

Ebner-Fairchild

article master with change
ebner-fairchild ef-master ef-change_1

Hung-Berns

article master with change
hung-berns hung-berns-master hung-berns-change

Munsell, level 5

article master with change
munsell5 munsell-master munsell-change

Rec. 2020 (HDR) gamut

article master with change
gamut gamut-master gamut-change
eonadler commented 5 years ago

Thank you both for your insightful comments.

My group is interested in JzAzBz because it is perceptually uniform under a Euclidean metric. So I'm not concerned about the specific coordinate ranges, even if they are different with respect to the paper, if they just reflect linear transformations of the coordinate system.

So, given that the change suggested by @jrus is a rescaling, do you think the coordinate range issue is just a matter of convenience? To me, the Rec. 2020 gamut in @nschloe 's final plot just looks rotated with respect to master, which isn't an issue for my use case.

jamadagni commented 5 years ago

I simply multiplied the JzAzBz output by 598.1807563064359 which is 100 divided by the Jz component corresponding to RGB triplet (255,255,255). I find the resulting numbers nice to compare with the output of the LAB/CAM02/CAM16:

        RGB |                           LAB |                      CAM02UCS |                      CAM16UCS |                 JZAZBZ-SCALED
  0   0   0 |    0.0000    0.0000    0.0000 |    0.0000    0.0000    0.0000 |    0.0000    0.0000    0.0000 |    0.0000    0.0000    0.0000
  0   0 255 |   32.3026   79.2022 -107.8533 |   31.2239   -8.3659  -39.1590 |   36.2552    8.5896  -37.8669 |   41.4225  -18.4584  -93.4991
  0 255   0 |   87.7370  -86.1777   83.1908 |   87.0057  -32.2317   30.4331 |   86.5506  -35.4870   27.5038 |   78.8833  -55.5455   60.1641
  0 255 255 |   91.1165  -48.0717  -14.1263 |   90.2576  -28.4179   -9.2366 |   90.6384  -28.5505   -8.5056 |   86.2685  -36.2323  -15.9889
255   0   0 |   53.2329   80.0906   67.2008 |   60.0487   38.6798   24.3153 |   59.1734   40.8207   21.1519 |   59.1962   59.6086   54.5743
255   0 255 |   60.3199   98.2421  -60.8341 |   66.6790   36.4558  -21.1075 |   67.3853   40.2178  -19.1223 |   70.3016   56.2463  -45.9072
255 255   0 |   97.1382  -21.5641   94.4861 |   97.4097  -10.3133   35.6083 |   96.8014  -12.7831   33.0375 |   94.1758  -14.4070   68.9318
255 255 255 |  100.0000    0.0000    0.0000 |  100.0000   -1.9170   -1.1378 |  100.0000   -1.8976   -1.0728 |  100.0000   -0.0802   -0.0493
daT4v1s commented 5 years ago

there seems to be an error in the reverse part of the model

y = (y_ + (self.g - 1) * x) / self.g
y = ((z_ * self.b - z_ + x_) * self.g + (y_ - z_) * self.b + z_ - x_) / (self.b * self.g)

i couldn't get the reverse transform to line up otherwise (arduous)

nschloe commented 5 years ago

@daT4v1s Could you open a new issue for that?

nschloe commented 5 years ago

@daT4v1s Forget about it, that's a non-issue. Your alternative expression is exactly the same as the original one. Open an issue if there still is something wrong.

eonadler commented 5 years ago

I'm closing this, since based on the helpful diagnostic plots from @nschloe, I believe that the normalization/range issue is purely cosmetic, and does not affect the relations between JzAzBz coordinates.

nschloe commented 5 years ago

@eonadler It'd be great to find out what's wrong; there seems something fishy about the Rec.2020 plots in the article.

jrus commented 5 years ago

normalization/range issue is purely cosmetic, and does not affect the relations between JzAzBz coordinates

This is a clearly false statement. But do what you like.

It would be nice to hear back from the authors of the paper, since their intended meaning is not very clear in the text.

KelSolaar commented 5 years ago

Haven't fully read the thread yet but:

It’s not clear if the best way to use the color space is to scale white to 10000, or instead to leave the maximum at whatever the actual luminance of the display is.

Keep in mind that PQ is an absolute EOTF, there is no free parameter for the display peak luminance, it has been modeled with Barten (1999) CSF thus there is nothing relative about it.

There should be no reason for the slightly different constants used by Safdar et al. to modify this postulate. The resulting curve is not very different from PQ:

image

I don't think it was a good idea to change it because the resulting curve does not agree with the CSF anymore.

jrus commented 5 years ago

There should be no reason for the slightly different constants used by Safdar et al. to modify this postulate. The resulting curve is not very different from PQ:

The curve in Safdar & al.’s paper has a somewhat different purpose from the PQ curve as far as I can tell (they just used that one as a starting point for convenience). The parameters they changed seem like they were optimized so that the full resulting color model would best match their experimental data.

Safdar & al. also have another paper https://doi.org/10.2352/issn.2169-2629.2018.26.96 but I cannot get access to a copy to read it.

KelSolaar commented 5 years ago

No it is the same, i.e. lightness prediction, they had to slightly modify it so that it fits best the data.

svgeesus commented 4 years ago

The “XYZ” coordinates they are using are counted in nits, and range from 0 to 10000. It’s not clear if the best way to use the color space is to scale white to 10000, or instead to leave the maximum at whatever the actual luminance of the display is.

I noticed that your implementation of Jzazbz scales sRGB white to 10,000 cd/m² which is very far from the correct value! Which explains why the results I got from your implementation bear no relation to the results from running the reference Safdar et al Matlab code.

Jzazbz uses a (slightly modified) Dolby PQ transfer function, which has enormous headroom for highlights. There is currently only one, research, display in the world which can actually produce a 10,000 cd/m² highlight (and then, only on a small part of the screen and only for a limited time). Commercial HDR grading monitors have a peak small-area luminance of 4,000or 2,000 cd/m². Commercial HDR TV conform to the VESA DisplayHDR standards, the most stringent grade of which requires a minimum of 1,400 cd/m² for a short-duration, 10% Center Patch test. Meanwhile the sustained, full-screen brightness test requires only 900 cd/m² (again, at the highest grade; the lowest grade requires only 320 cd/m².

Since the broadcast TV world has been producing HDR content for some time and has considered mixture of SDR and HDR content and grading monitors, there are already standards and informative reports which define how to map SDR (paper or media or diffuse reflector) white onto PQ-encoded HDR.

Rep. ITU-R BT.2390-8 section 5.3.1 describes a peak luminance for SDR (from BT.709 and BT.1886) of 100 cd/m² compared to the PQ peak of 10,000 cd/m².

Meanwhile Dolby, in Reference Level Guidelines for PQ (BT.2100), working from the PQ value of 0.34 for an 18% grey card, state that diffuse white at 0.54 PQ value corresponding to a luminance of 140 cd/m².

I find the Dolby analysis more detailed and convincing, and thus suggest that all SDR color spaces (sRGB, AdbeRGB, Display P3, etc) have their peak white, which is a full-screen long duration diffuse white, mapped to 140 cd/m² when converting into PQ-based colorspaces like BT.2100 (Bt.2020 primaries and PQ transfer curve) or Jzazbz.

Mapping it to 10,000 means you are asking for a blinding white which cannot be displayed. It also means you have zero headroom for specular highlights, defeating the entire point of HDR in general and PQ in particular.

nschloe commented 4 years ago

@svgeesus Thanks for the thorough analysis. Could you run some "random" numbers through the upstream MATLAB script and post them here? I'd love to make sure that colorio gives the same output.

Edit: I ran the scripts with Octave and colorio.JzAzBz() returns the same results.

jrus commented 4 years ago

@svgeesus You’ll notice there is a line in there:

display_white_luminance = 10000

Which can trivially be changed (and which I have fiddled around in various experimenting; a previous published version of this notebook had this set to a value of 200, and I have reset it to that). This only has an effect on my inessential added srgb_to_xyz_abs function, and is outside the scope of the reference implementation. I changed it as part of a local experiment and never changed it back.

results I got from your implementation bear no relation to the results from running the reference Safdar et al Matlab code.

I would hope not. The implementation of the Jzazbz model and its inverse (from XYZ coordinates) should be exactly identical to the paper / Matlab / etc. If it is not, please tell me. The Matlab code provided by Safdar & al. does not provide any method at all for obtaining XYZ inputs, but if you put the same XYZ inputs into the Matlab code and into my Javascript you should get identical results.

What was not clear to me is precisely how Safdar & al. expect their model to be scaled for non-HDR displays, and how their data fitting of existing colorimetric data was handled in construction of their model. I found their paper quite unclear/sparse about this general topic, with a lot of details of their data fitting left implicit/underspecified.

If you set this parameter to a value of 140 or 200, it’s not clear to me that the resulting gamut in a/b dimensions for an SDR sRGB display actually ends up particularly perceptually uniform. Go ahead and fiddle with it for yourself. For a couple particular uses I had in trying to use this tool to construct some color schemes in my own other experiments, I had better results when setting this to 10000 (it’s possible Jzazbz is just the wrong tool overall for my uses), but didn’t really mean to imply that it should be set that way for general use, and didn’t even intend to leave it set that way in a published notebook.

Jzazbz uses a (slightly modified) Dolby PQ transfer function

Dramatically changing the value of the exponent applied to the inputs makes a pretty big difference. But then they use this as just one step among several in their (inverse) model: the most important other one is a rational function applied to lightness. The result is that the shape is substantially different:

https://www.desmos.com/calculator/pu7ylakohj

P.S. if you don’t directly ping someone by name in a github discussion on someone else’s project, github doesn’t make a particularly obvious notification about it.

jrus commented 4 years ago

@svgeesus

results I got from your implementation bear no relation to the results from running the reference Safdar et al Matlab code.

I just double checked, and for every input I have tried results match to like 14 digits.

Here’s an example screenshot showing Matlab and Javascript:

agyrqnR

genmeblog commented 2 years ago

Just faced to the same problem. Found another implementation where white luminance parameter is 1

https://github.com/colour-science/colour/blob/develop/colour/models/jzazbz.py#L436