I'm still playing around with RGB gamut calculations in
$ L^* a^* b^* $
space. (See my last post on this topic,
"Visualizing out-of-gamut colors in a Lab curve."
) Today's post features some new gamut-related visualizations, plus some computational tricks involving gamut boundaries and rays in
$ L^* a^* b^* $
space.
First, here is another way to communicate the idea that the in-gamut area in the
$ (a^*,b^*) $
plane varies with
$ L^* $
(perceptual lightness). For 9 values of
$ L^* $
, (10, 20, ..., 90), I'll compute a 2-D
$ (a^*,b^*) $
gamut mask by brute-forcing it. Then, I'll use overlaid contour plots to show the variation in gamut boundaries.
[aa,bb,LL] = meshgrid(a,b,L);
rgb = lab2rgb(cat(3,LL(:,:,k),aa(:,:,k),bb(:,:,k)));
mask = all((0 <= rgb) & (rgb <= 1),3) * 2 - 1 + L(k);
contour(a,b,mask,[L(k) L(k)],LineColor=[.8 .8 .8],LineWidth=1.5,ShowText=true,...
title("Gamut boundary in the (a,b) plane for several values of L*")
Here's another visualization concept. People often show colors on the
$ (a^*,b^*) $
plane, to give an idea of the meaning of
$ a^* $
and
$ b^* $
, but that doesn't communicate very well the idea that there are usually multiple colors, corresponding to various
$ L^* $
values, at any one
$ (a^*,b^*) $
location. Below, I show both the brightest in-gamut color and the darkest in-gamut color at each
$ (a^*,b^*) $
location.
[L_min(p,q),L_max(p,q)] = Lrange(aa(p,q),bb(p,q));
rgb = lab2rgb(cat(3,L_max,aa,bb));
imshow(rgb,XData=a([1 end]),YData=b([1 end]))
title("Brightest in-gamut color")
rgb_min = lab2rgb(cat(3,L_min,aa,bb));
imshow(rgb_min,XData=a([1 end]),YData=b([1 end]))
title("Darkest in-gamut color")
Next, I find myself sometimes wanting to draw a ray in
$ L^* a^* b^* $
space and find the gamut boundary location along that ray. To that end, I wrote a simple utility function (
findNonzeroBoundary
below) that performs a binary search to find where a function goes from positive to 0. Then, I wrote some anonymous functions to find the desired gamut boundary point. Specifically, I was interested in this question: For a given
$ L^* $
value and a given
$ (a^*,b^*) $
plane angle,
h
, what is the in-gamut color with the maximum chroma,
c
, or distance from
$ (0,0) $
in the
$ (a^*,b^*) $
plane?
Fair warning
: the code below gets tricky with anonymous functions. You might hate it. If so, I totally understand, and I hope you'll forgive me. :-)
I'll start by creating an anonymous function that converts from
$ L^* c h $
to
$ L^* a^* b^* $
:
lch2lab = @(lch) [lch(1) lch(2)*cosd(lch(3)) lch(2)*sind(lch(3))];
Next, here is an anonymous function that returns whether or not a particular
$ L^* a^* b^* $
point is in gamut.
inGamut = @(lab) all(0 <= lab2rgb(lab),2) & all(lab2rgb(lab) <= 1,2);
Finally, a third anonymous function uses
findNonzeroBoundary
to find the gamut boundary point that I'm interested in.
maxChromaAtLh = @(L,h) findNonzeroBoundary(@(c) inGamut(lch2lab([L c h])), 0, 200);
Let's exercise this function to find a high chroma dark color at
$ h=0^{\circ} $
.
And here's what that color looks like.
rgb_out = lab2rgb(lch2lab([L c h]));
What happens when we try to find a high chroma color, at the same hue angle, that is bright instead of dark?
You can can see that the maximum
c
value is much lower for the higher value of
$ L^* $
. What does it look like?
rgb_out = lab2rgb(lch2lab([L c h]));
When I was doing these experiments to prepare for this blog post, my original intent was to just show examples for a couple of different values of
h
and
$ L^* $
. But I couldn't stop! It was too much fun, and I kept trying different values.
After about 15 minutes or so, I decided it would be best to write some simple loops to generate a relatively large number of
$ (L^*,h) $
combinations to look at. Here's the code to generate the high
c
colors for a variety of
$ (L^*,h) $
combinations.
rgb = zeros(length(h),length(L),3);
c = maxChromaAtLh(L(q),h(k));
rgb(k,q,:) = lab2rgb(lch2lab([L(q) c h(k)]));
And here's the code to view all those colors on a grid, with labeled
h
and
$ L^* $
axes.
rgb2 = reshape(fliplr(rgb),[],3);
p = colorSwatches(rgb2,[length(L) length(h)]);
p.XData = (p.XData - 0.5) * (dh/1.5) + h(1);
p.YData = (p.YData - 0.5) * (dL/1.5) + L(1);
title("Highest chroma (most saturated) colors for different L* and h values")
Utility Functions
function[L_min,L_max] = Lrange(a,b)
gamut_mask = all((0 <= rgb) & (rgb <= 1),2);
j = find(gamut_mask,1,'first');
k = find(gamut_mask,1,'last');
functionx = findNonzeroBoundary(f,x1,x2,abstol)
abstol(1,1) {mustBeFloat}= 1e-4
if(f(x1) == 0) || (f(x2) ~= 0)
error("Function must be nonzero at initial starting point and zero at initial ending point.")
ifabs(xm - x1) / max(abs(xm),abs(x1)) <= abstol
x = findNonzeroBoundary(f,x1,xm);
x = findNonzeroBoundary(f,xm,x2);
Comments
To leave a comment, please clickhereto sign in to your MathWorks Account or create a new one.