colour-science / colour

Colour Science for Python
https://www.colour-science.org
BSD 3-Clause "New" or "Revised" License
2.14k stars 263 forks source link

[BUG]: Problems converting "sRGB" to "Munsell Renotation System". #1173

Closed notevenstickmen closed 1 month ago

notevenstickmen commented 1 year ago

Question

I'm having problems converting some colours from sRGB to Munsell. I'm no expert in this field so can well believe I'm doing something daft but I don't know what.

I have a spreadsheet (2547 rows) of the main Munsell colours with their sRGB equivalents (I think this came via Paul Centore's work; the numbers check back to this anyway). I've written routine to convert sRGB to Munsell & have been using this spreadsheet to check my results.

Code: import colour import numpy as np rgb = np.array([141,116,121]) XYZ = colour.sRGB_to_XYZ(rgb / 255) xyY = colour.XYZ_to_xyY(XYZ) munsell = colour.xyY_to_munsell_colour(xyY, chroma_decimals=1)

Issues:

  1. I have about 55 Assertation errors e.g. Most of the colours here seem to be relatively low value & chroma but not all e.g. 02.5RP-9-02 RGB(241,222,239) AssertionError: ""array([ 3.08954081, 9.02276054, 1.84797444, 8. ])"" specification chroma must be normalised to domain [2, 50]! The 4th argument looks v strange to me & all cases have this format i.e. n.spaces.

  2. I'm getting quite a lot of hue discrepances in those areas where one hue family changes to another. e.g.

10.0R -7-08 RGB( 242,150,128) my result: 0.1YR 7.0/8.0 10.0R -7-10 RGB( 254,144,114) my result: 0.1YR 7.0/9.9 10.0R -7-12 RGB( 255,136,97) my result: 0.6YR 6.8/11.0

I most cases, the sub-hue group in my result is < 1 rather than 10.0.

Can anyone advise me?

glenndavis52 commented 1 year ago

The 5 principal and 5 intermediate hues form 10 arcs on the hue circle. The arcs do not intersect (except at the endpoints) and cover the hue circle. Each arc is parameterized from 0 to 10. Hues 10R and 0YR are endpoints of adjacent arcs and denote exactly the same hue. The Munsell system prefers the former notation. The 4'th number 8 in the assertion is the Hue Index of intermediate hue RP.

I claim that XYZ should really be chromatically adapted from Illuminant D65 (the sRGB standard) to Illuminant C (with which the Munsell system was designed). So there should be another conversion (a chromatic adaptation transform) between sRGB_to_XYZ() and XYZ_to_xyY(). As a test, when R=G=B, then xy should be about (0.3101,0.3163), and not (0.3127,0.3290).

I do not know anything about the apparent requirement that Chroma is in [2,50].

jaimecorton commented 6 months ago

I am having the same problems even using the Illuminant C mentioned by glenndavis52, my code is the following:

def RGB2Munsell(RGB):

    # RGB is expected to have the following format: (R, G, B), where RGB values range from 0 to 1
    # e.g: RGB = (0.96820063, 0.74966853, 0.60617991)
    C = colour.CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["C"]

    Munsell = colour.xyY_to_munsell_colour(colour.XYZ_to_xyY(colour.sRGB_to_XYZ(RGB, C)))

    return Munsell

By introducing as sRGB the following array: array([ 0.5, 0.5, 0.5]) (Trying to achieve the testing of R=G=B) I get the same error AssertionError: "array([ 5.42874897e+00, 5.23531097e+00, 1.08571452e-03, 7.00000000e+00])" specification chroma must be normalised to domain [2, 50]!

jaimecorton commented 6 months ago

@notevenstickmen I've been looking at it (https://colour.readthedocs.io/en/develop/_modules/colour/notation/munsell.html) and it seems that our error is comming from the fact that on the colour.xyY_to_munsell_colour() step, the library internally converts xyY to munsell specification and then munsell specification to munsell colour:

`

def xyY_to_munsell_colour( xyY: ArrayLike, hue_decimals: int = 1, value_decimals: int = 1, chroma_decimals: int = 1, ) -> str | NDArrayStr: """ Convert from CIE xyY colourspace to Munsell colour.

Parameters
----------
xyY
    *CIE xyY* colourspace array.
hue_decimals
    Hue formatting decimals.
value_decimals
    Value formatting decimals.
chroma_decimals
    Chroma formatting decimals.

Returns
-------
:class:`str` or :class:`numpy.NDArrayFloat`
    *Munsell* colour.

Notes
-----
+------------+-----------------------+---------------+
| **Domain** | **Scale - Reference** | **Scale - 1** |
+============+=======================+===============+
| ``xyY``    | [0, 1]                | [0, 1]        |
+------------+-----------------------+---------------+

References
----------
:cite:`Centorea`, :cite:`Centore2012a`

Examples
--------
>>> xyY = np.array([0.38736945, 0.35751656, 0.59362000])
>>> xyY_to_munsell_colour(xyY)
'4.2YR 8.1/5.3'
"""

specification = to_domain_10(
    xyY_to_munsell_specification(xyY), _munsell_scale_factor()
)
shape = list(specification.shape)
decimals = (hue_decimals, value_decimals, chroma_decimals)

munsell_colour = np.reshape(
    np.array(
        [
            munsell_specification_to_munsell_colour(a, *decimals)
            for a in np.reshape(specification, (-1, 4))
        ]
    ),
    shape[:-1],
)

return str(munsell_colour) if shape == [4] else munsell_colour

`

If we see the function munsell_specification_to_munsell_colour in detail we will find where the error is popping:

`

def munsell_specification_to_munsell_colour( specification: ArrayLike, hue_decimals: int = 1, value_decimals: int = 1, chroma_decimals: int = 1, ) -> str: """ Convert from Munsell Colorlab specification to given Munsell colour.

Parameters
----------
specification
    *Munsell* *Colorlab* specification.
hue_decimals
    Hue formatting decimals.
value_decimals
    Value formatting decimals.
chroma_decimals
    Chroma formatting decimals.

Returns
-------
:class:`str`
    *Munsell* colour.

Examples
--------
>>> munsell_specification_to_munsell_colour(np.array([np.nan, 5.2, np.nan, np.nan]))
'N5.2'
>>> munsell_specification_to_munsell_colour(np.array([10, 2.0, 4.0, 7]))
'10.0R 2.0/4.0'
"""

hue, value, chroma, code = tsplit(normalise_munsell_specification(specification))

if is_grey_munsell_colour(specification):
    return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)
else:
    hue = round(hue, hue_decimals)
    attest(
        0 <= hue <= 10,
        f'"{specification!r}" specification hue must be normalised to '
        f"domain [0, 10]!",
    )

    value = round(value, value_decimals)
    attest(
        0 <= value <= 10,
        f'"{specification!r}" specification value must be normalised to '
        f"domain [0, 10]!",
    )

    chroma = round(chroma, chroma_decimals)
    attest(
        2 <= chroma <= 50,
        f'"{specification!r}" specification chroma must be normalised to '
        f"domain [2, 50]!",
    )

    code_values = MUNSELL_HUE_LETTER_CODES.values()
    code = round(code, 1)
    attest(
        code in code_values,
        f'"{specification!r}" specification code must one of "{code_values}"!',
    )

    if value == 0:
        return MUNSELL_GRAY_EXTENDED_FORMAT.format(value, value_decimals)
    else:
        hue_letter = MUNSELL_HUE_LETTER_CODES.first_key_from_value(code)

        return MUNSELL_COLOUR_EXTENDED_FORMAT.format(
            hue,
            hue_decimals,
            hue_letter,
            value,
            value_decimals,
            chroma,
            chroma_decimals,
        )

`

Specifically the error is produced here:

`

chroma = round(chroma, chroma_decimals) attest( 2 <= chroma <= 50, f'"{specification!r}" specification chroma must be normalised to ' f"domain [2, 50]!", )

`

But I will need further assistance with why that is happening when going from sRGB to Munsell colour, some help with it wil be much appreciated. I am using the colour.CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["C"] illuminant. There was a ColourWarning regarding ColourUsageWarning: "array([ 0.37326798, 0.37285041, 0.09701717])" is not within "MacAdam" limits for illuminant "C"! For the moment what I am doing is the following modification since it solves the problem of a Chroma under 2 e.g: Chroma = 1.8, but I would like to have a better solution that requires less original colours modification:

` def RGB2Munsell(RGB):

# RGB is expected to have the following format: (R, G, B), where RGB values range from 0 to 1
# e.g: RGB = (0.96820063, 0.74966853, 0.60617991)
C = colour.CCS_ILLUMINANTS["CIE 1931 2 Degree Standard Observer"]["C"] # C or D65

XYZ = colour.sRGB_to_XYZ(RGB, C)
xyY = colour.XYZ_to_xyY(XYZ)
MunsellSpecification = colour.notation.munsell.xyY_to_munsell_specification(xyY)
if MunsellSpecification[2]<2:
    MunsellSpecification[2] = np.ceil(MunsellSpecification[2])
Munsell = colour.notation.munsell.munsell_specification_to_munsell_colour(MunsellSpecification) 

return Munsell

`

KelSolaar commented 6 months ago

Hello,

This is a side effect of using the colour.colour.sRGB_to_XYZ definition which introduces minor precision issues because our sRGB colourspace uses the 4 decimal places rounded matrices from IEC 61966-2-1:1999: https://github.com/colour-science/colour/blob/develop/colour/models/rgb/datasets/srgb.py#L67

The consequence is a loss of precision and the CIE xyY value becomes [ 0.31007559 0.31616194 0.21404116] instead of [ 0.31006 0.31616 0.21404114] which is perfectly neutral. This fails under the grey threshold test here: https://github.com/colour-science/colour/blob/develop/colour/notation/munsell.py#L1076 I think I will increase the threshold value and make it a global that can be changed also.

For the time being you could to the sRGB conversion as follows because BT.709 uses a computed matrix and will not suffer from precision issues:

xyY = colour.XYZ_to_xyY(colour.RGB_to_XYZ(colour.cctf_decoding([0.5, 0.5, 0.5]), colour.RGB_COLOURSPACES["ITU-R BT.709"], C))
KelSolaar commented 6 months ago

The develop branch should now have reduced threshold value.

dedores commented 2 months ago

Hi all, I tried the workaround presented in the recent response, but still get the same error. I am using version 0.4.4 of Colour-Science. My original RGB value on a 255-scale is [94, 89, 88]. I use that array as a replacement for the entirety of the "colour.cctf_decoding([0.5, 0.5, 0.5])" term in the recent response, but am still receiving the "specification chroma must be normalised to domain [2, 50]".

For added context, the input array for "colour.notation.munsell.munsell_specification_to_munsell_colour" is [ 8.71423617, 6.49948406, 1. , 7. ].

I'm wondering what I might be doing wrong in implementing this solution? My apologies in advance if there is a self-evident solution I'm missing here, I'm a bit new to using the Colour-Science capabilities. Thanks for any help you can provide!

glenndavis52 commented 2 months ago

On Fri, Sep 13, 2024 at 7:11 PM dedores @.***> wrote:

Hi all, I tried the workaround presented in the recent response, but still get the same error. I am using version 0.4.4 of Colour-Science. My original RGB value on a 255-scale is [94, 89, 88]. I use that array as a replacement for the entirety of the "colour.cctf_decoding([0.5, 0.5, 0.5])" term in the recent response, but am still receiving the "specification chroma must be normalised to domain [2, 50]".

Judging by this code fragment from munsell.py chroma = round(chroma, chroma_decimals) attest( 2 <= chroma <= 50, f'"{specification!r}" specification chroma must be normalised to ' f"domain [2, 50]!", ) it appears that Chroma less than 2 is not supported.

Using this online color converter:

http://gluonics.com:85/converter.html

and entering RGB = [94, 89, 88] (then press the "signal RGB" button), I get Munsell HVC = 0.32YR 3.8/0.37 . So the correct Chroma is 0.37, which is less than 2.

Glenn

dedores commented 2 months ago

Hi Glenn, thanks for the quick response! That makes sense. It does appear that these values are not supported. Thanks for taking a look at this.

KelSolaar commented 1 month ago

Closing this one as it should be fixed.

simonpalmer commented 1 month ago

Been watching this in the background as I am having the same issue.

@KelSolaar can you elaborate on "should be"?

Do you mean it has been fixed by other code changes and will appear in the next release?

simonpalmer commented 1 month ago

nvm, figured it out...