mail@pastecode.io avatar
a month ago
7.0 kB

% It was a while ago now, but on
% <https://blogs.mathworks.com/steve/files/illuminant-d65.png April 27>
% I started explaining how I made this plot, which is from
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> (_Digital Image Processing Using MATLAB_, 3rd ed.):
% <<https://blogs.mathworks.com/steve/files/illuminant-d65.png>>
% In today's follow-up, I'll discuss how I computed the colors of the
% spectrum to display below the x-axis. I will use and refer to several
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> functions. These are available to you in _MATLAB Color
% Tools_ on the
% <https://www.mathworks.com/matlabcentral/fileexchange/64161-matlab-color-tools
% File Exchange> and on <https://github.com/mathworks/matlab-color-tools
% GitHub>. The entire set of
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> functions is also on
% <https://github.com/dipum/dipum-toolbox GitHub>.
%% Visible wavelength
% What should the x-axis limits be on this plot? In other words, what is
% the visible wavelength that we are interested in? You will see
% different limits being used in different places. The limits used here,
% 380 nm to 780 nm, are those given in Berns, R. S. (2000). _Billmeyer
% and Saltzman's Principals of Color Technology_, 3rd ed., John Wiley &
% Sons, NJ.

lambda = 380:780;

%% Find XYZ values for the spectral colors
% The first computational step is to find the XYZ values for each value
% of lambda. This computation can be found in the
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> function |lambda2xyz|. But it is really simple: just
% interpolate into the CIE XYZ color matching functions, which are
% returned by the <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> function |colorMatchingFunctions|.

T = colorMatchingFunctions;

% To find the XYZ values for a specific lambda, such as 500, we can use
% |interp1|:


% Or we can find the XYZ values for all of the wavelengths we are
% interested in.
XYZ = interp1(T.lambda,[T.x,T.y,T.z],lambda(:));


title("XYZ values for spectral wavelengths")

%% Try a simple XYZ to RGB conversion
% Let's try simply converting these XYZ values directly to RGB using the
% Image Processing Toolbox function |xyz2rgb|.

RGB = xyz2rgb(XYZ);

% (The function |drawColorbar| is below. It uses the
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> function |colorSwatches|.)
% That doesn't look like a very good spectral color plot to me. It seems
% uneven, with several patches that seem to be mostly one color. What's
% going on with this? We can see the problem if we draw a line plot of
% the RGB values. (The |plotrgb| and |shadeGamutRegion| functions are
% down below.)


% The gray shaded region shows the range between 0 and 1; this is the
% displayable range of colors. Everything outside that range (negative
% values, or values greater than 1), can't be displayed exactly. These
% out-of-range values are being clipped to the displayable range, and
% that leads to a bad results.
% I'm going to show you the method used by the
% <http://www.imageprocessingplace.com/DIPUM-3E/dipum3e_main_page.htm
% *DIPUM3E*> function |spectrumColors|. The method is a variation of the
% one described in: Andrew Young (2012). _Rendering Spectra_
% (https://aty.sdsu.edu/explain/optics/rendering.html). Retrieved July
% 16, 2020.

%% Work with linear RGB values
% First, let's convert the XYZ values to "linear" RGB values. The
% typical RGB values we see for image pixels are related nonlinearly to
% light intensity, and linear RGB values are more appropriate for the
% following averaging and scaling steps. The Image Processing Toolbox
% function |xyz2rgb| can optionally convert to linear values.

RGB_lin = xyz2rgb(XYZ,'ColorSpace','linear-rgb');
title("Linear RGB values for spectral wavelengths")

%% Heuristic scaling of linear RGB values
% We want to modify those curves so that they fall within the range
% [0,1] and produce a reasonably accurate, smoothly varying, and
% attractive representation of the spectral colors.
% The next thing we'll do is scale so that the maximum linear RGB value
% is 1.0. (Note: Young (2012) divides by a fixed value of 2.34.)
RGB_lin = RGB_lin / max(RGB_lin(:));


% Now, one component at a time, and for each spectral color, mix in a
% sufficient amount neutral gray with the same Y to bring negative
% component values up to 0.
Y = XYZ(:,2);
for k = 1:3
   C = RGB_lin(:,k);
   F = Y ./ (Y - C);
   % No scaling is needed for component values that are already
   % nonnegative.
   F(C >= 0) = 1;
   RGB_lin = Y + F.*(RGB_lin - Y);


% Next, to get brighter spectral colors, including a good yellow, scale
% up the linear RGB values, allowing them to get higher than 1.0. Then,
% for each color, scale all components back down, if necessary, so that
% the maximum component value is 1.0. Note: [Young 2012] uses a scale
% factor of 1.85.

RGB_lin = RGB_lin * 2.5;
S = max(RGB_lin,[],2);
S = max(S,1);
RGB_lin = RGB_lin ./ S;


%% Smooth the curves
% Smooth out the linear RGB curves to eliminate discontinuous first
% derivatives. This helps the spectrum to appear smoother, reducing
% sharp transition points. Note: This step is not in [Young 2012].
RGB_lin = conv2(RGB_lin,ones(21,1)/21,'same');

% Eliminate small negative numbers and numbers slightly greater than 1
% that have been introduced through floating-point round-off.
RGB_lin = min(max(RGB_lin,0),1);

%% Convert to nonlinear RGB values for the final result
% Convert to nonlinear sRGB values suitable for display on a computer
% monitor.
RGB = lin2rgb(RGB_lin);



% Next time, I'll talk about how to draw the spectral color scale
% underneath the plot.

%% Utility functions
function drawColorbar(rgb_colors)
f = figure;
f.Position(4) = f.Position(4) / 5;
daspect([40 1 1])
axis off

function plotrgb(x,rgb_colors)
% Pick the colors we want to use from the normal line color order.
c = lines(7);
blue = c(1,:);
red = c(2,:);
green = c(5,:);
hold on
hold off
grid on
axis tight
yl = ylim;
ylim([min(yl(1)-0.05,-0.05) max(yl(2)+0.05,1.05)])

xlabel("wavelength (nm)")


function shadeGamutRegion
xl = xlim;
xx = [xl(1) xl(2) xl(2) xl(1) xl(1)];
yy = [0 0 1 1 0];
p = patch(xx,yy,[0.5 0.5 0.5],"FaceAlpha",0.1,...

% _Copyright 2020 The MathWorks, Inc._