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');
function x = 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.")
if abs(xm - x1) / max(abs(xm),abs(x1)) <= abstol
x = findNonzeroBoundary(f,x1,xm);
x = findNonzeroBoundary(f,xm,x2);
Comments
To leave a comment, please click here to sign in to your MathWorks Account or create a new one.