2727import math
2828from functools import lru_cache
2929from . import Fit , clip_channels
30+ from .fit_raytrace import get_conversion_matrices
3031from .. import util
31- from ..cat import WHITES , calc_adaptation_matrices , Bradford
32+ from ..cat import WHITES , CAT
3233from .. import algebra as alg
3334from ..spaces import Space , RGBish
34- from ..types import Vector , VectorLike , Matrix
35- from ..spaces .oklab import LMS_TO_XYZD65 , OKLAB_TO_LMS3
35+ from ..types import VectorLike , Matrix
36+ from ..spaces .oklab import OKLAB_TO_LMS3
3637from typing import Any , TYPE_CHECKING # noqa: F401
3738
3839if TYPE_CHECKING : #pragma: no cover
@@ -51,32 +52,42 @@ def first_root(coeff: VectorLike, mn: float, mx: float) -> float:
5152 return best
5253
5354
54- def get_hue_data (name : str , cs : Space , h : float ) -> tuple [Vector , Vector , Vector , float , list [float ]]:
55+ def get_hue_data (name : str , cs : Space , cat : str , adapt : CAT , h : float ) -> tuple [Matrix , float , list [float ]]:
5556 """
5657 Get hue data.
5758
5859 At fixed L and H, each linear RGB channel is exactly cubic in chroma c:
5960
6061 ```
61- channelᵢ(c) = L³ + 3L²· Aᵢ·c + 3L·Bᵢ·c² + Dᵢ·c³
62+ channelᵢ(c) = Aᵢ·c³ + 3L·Bᵢ·c² + 3L²· Dᵢ·c + L ³
6263 ```
6364
6465 because `l'ₖ = L + Qₖ·c` is affine in c (the a, b axes enter Oklab→LMS
6566 linearly), and `channelᵢ = Σₖ Tᵢₖ·l'ₖ³`. The constant is `L³` since `c = 0` is
6667 gray and the rows of LMS to RGB sum to 1.
6768 """
6869
69- m = get_lms_to_rgb (name , cs )
70+ m = get_conversion_matrices (name , cs , cat , adapt )[ 0 ]
7071 rad = math .radians (h )
71- q = alg .matmul_x3 (OKLAB_TO_LMS3 , [0 , math .cos (rad ), math .sin (rad )], dims = alg .D2_D1 )
72- a = alg .matmul_x3 (m , q , dims = alg .D2_D1 )
73- b = alg .matmul_x3 (m , [x * x for x in q ], dims = alg .D2_D1 )
74- d = alg .matmul_x3 (m , [x ** 3 for x in q ], dims = alg .D2_D1 )
72+ q1 , q2 , q3 = alg .matmul_x3 (
73+ OKLAB_TO_LMS3 ,
74+ [0 , math .cos (rad ), math .sin (rad )],
75+ dims = alg .D2_D1
76+ )
77+ m2 = alg .matmul_x3 (
78+ m ,
79+ [
80+ [q1 ** 3 , q1 ** 2 , q1 ],
81+ [q2 ** 3 , q2 ** 2 , q2 ],
82+ [q3 ** 3 , q3 ** 2 , q3 ]
83+ ],
84+ dims = alg .D2
85+ )
7586
7687 # Substituting `c = L·t` factors L out entirely: `channelᵢ(c) = L³·Pᵢ(t)` with
7788 #
7889 # ```
79- # Pᵢ(t) = 1 + 3Aᵢ·t + 3Bᵢ·t² + Dᵢ·t³
90+ # Pᵢ(t) = Aᵢ·t³ + 3Bᵢ·t² + 3Dᵢ·t + 1
8091 # ```
8192 #
8293 # so `Pᵢ` — hence the lower-gamut exit and the monotonicity structure — depends
@@ -89,32 +100,17 @@ def get_hue_data(name: str, cs: Space, h: float) -> tuple[Vector, Vector, Vector
89100 t_lower = math .inf
90101 # First turning point of each channel in t-space (infinity if monotonic)
91102 turn = [0.0 ] * 3
92- for i in range ( 3 ):
93- t_lower = min (t_lower , first_root ([d [ i ], 3 * b [ i ], 3 * a [ i ], 1 ], 1e-9 , 1 ))
94- turn [i ] = first_root ([0 , d [ i ], 2 * b [ i ], a [ i ]], 1e-12 , math .inf )
95- return a , b , d , t_lower , turn
103+ for i , v in enumerate ( m2 ):
104+ t_lower = min (t_lower , first_root ([v [ 0 ], 3 * v [ 1 ], 3 * v [ 2 ], 1 ], 1e-9 , 1 ))
105+ turn [i ] = first_root ([0 , v [ 0 ], 2 * v [ 1 ], v [ 2 ]], 1e-12 , math .inf )
106+ return m2 , t_lower , turn
96107
97108
98109@lru_cache (maxsize = 1024 )
99- def get_hue_data_cached (name : str , cs : Space , h : float ) -> tuple [Vector , Vector , Vector , float , list [float ]]:
110+ def get_hue_data_cached (name : str , cs : Space , cat : str , adapt : CAT , h : float ) -> tuple [Matrix , float , list [float ]]:
100111 """Get hue data but use a hue_cache."""
101112
102- return get_hue_data (name , cs , h )
103-
104-
105- @lru_cache (maxsize = 10 )
106- def get_lms_to_rgb (name : str , space : Space ) -> Matrix :
107- """Get LMS to RGB matrix."""
108-
109- d65 = WHITES ['2deg' ]['D65' ]
110- m = space .TO_RGB # type: ignore[attr-defined]
111- if space .WHITE != d65 :
112- m = alg .matmul_x3 (
113- m ,
114- calc_adaptation_matrices (d65 , space .WHITE , Bradford .MATRIX ),
115- dims = alg .D2
116- )
117- return alg .matmul_x3 (m , LMS_TO_XYZD65 , dims = alg .D2 )
113+ return get_hue_data (name , cs , cat , adapt , h )
118114
119115
120116class OkLChCubic (Fit ):
@@ -170,19 +166,23 @@ def fit(
170166 color .update ('xyz-d65' , WHITE )
171167 return
172168
173- a , b , d , t_lower , turn = get_hue_data_cached (space , cs , h ) if cache else get_hue_data (space , cs , h )
169+ cat = color .CHROMATIC_ADAPTATION
170+ adapt = color .CAT_MAP [cat ]
171+ m , t_lower , turn = (
172+ get_hue_data_cached (space , cs , cat , adapt , h ) if cache else get_hue_data (space , cs , cat , adapt , h )
173+ )
174174 # Work in `t = c/L`. The cap starts at the input chroma and the (hue-only) lower
175175 # exit; the white bound below can only pull it lower.
176176 max_t = min (c / l , t_lower )
177177 target = 1 / (l ** 3 )
178- for i in range ( 3 ):
178+ for i , v in enumerate ( m ):
179179 if turn [i ] > max_t :
180- if a [ i ] < 0 :
180+ if v [ 2 ] < 0 :
181181 continue
182- p_max_t = ((d [ i ] * max_t + 3 * b [ i ]) * max_t + 3 * a [ i ]) * max_t + 1
182+ p_max_t = ((v [ 0 ] * max_t + 3 * v [ 1 ]) * max_t + 3 * v [ 2 ]) * max_t + 1
183183 if p_max_t < target :
184184 continue
185- max_t = min (max_t , first_root ([d [ i ], 3 * b [ i ], 3 * a [ i ], 1 - target ], 1e-9 , max_t ))
185+ max_t = min (max_t , first_root ([v [ 0 ], 3 * v [ 1 ], 3 * v [ 2 ], 1 - target ], 1e-9 , max_t ))
186186
187187 mapcolor [1 ] = l * max_t
188188 clip_channels (mapcolor .convert (orig_space , in_place = True ))
0 commit comments