diff --git a/matlab/utilities/graphics/colorspace.m b/matlab/utilities/graphics/colorspace.m index 1aed58d43..dc46d5e85 100644 --- a/matlab/utilities/graphics/colorspace.m +++ b/matlab/utilities/graphics/colorspace.m @@ -1,515 +1,515 @@ -function varargout = colorspace(Conversion,varargin) -%COLORSPACE Transform a color image between color representations. -% B = COLORSPACE(S,A) transforms the color representation of image A -% where S is a string specifying the conversion. The input array A -% should be a real full double array of size Mx3 or MxNx3. The output B -% is the same size as A. -% -% S tells the source and destination color spaces, S = 'dest<-src', or -% alternatively, S = 'src->dest'. Supported color spaces are -% -% 'RGB' sRGB IEC 61966-2-1 -% 'YCbCr' Luma + Chroma ("digitized" version of Y'PbPr) -% 'JPEG-YCbCr' Luma + Chroma space used in JFIF JPEG -% 'YDbDr' SECAM Y'DbDr Luma + Chroma -% 'YPbPr' Luma (ITU-R BT.601) + Chroma -% 'YUV' NTSC PAL Y'UV Luma + Chroma -% 'YIQ' NTSC Y'IQ Luma + Chroma -% 'HSV' or 'HSB' Hue Saturation Value/Brightness -% 'HSL' or 'HLS' Hue Saturation Luminance -% 'HSI' Hue Saturation Intensity -% 'XYZ' CIE 1931 XYZ -% 'Lab' CIE 1976 L*a*b* (CIELAB) -% 'Luv' CIE L*u*v* (CIELUV) -% 'LCH' CIE L*C*H* (CIELCH) -% 'CAT02 LMS' CIE CAT02 LMS -% -% All conversions assume 2 degree observer and D65 illuminant. -% -% Color space names are case insensitive and spaces are ignored. When -% sRGB is the source or destination, it can be omitted. For example -% 'yuv<-' is short for 'yuv<-rgb'. -% -% For sRGB, the values should be scaled between 0 and 1. Beware that -% transformations generally do not constrain colors to be "in gamut." -% Particularly, transforming from another space to sRGB may obtain -% R'G'B' values outside of the [0,1] range. So the result should be -% clamped to [0,1] before displaying: -% image(min(max(B,0),1)); % Clamp B to [0,1] and display -% -% sRGB (Red Green Blue) is the (ITU-R BT.709 gamma-corrected) standard -% red-green-blue representation of colors used in digital imaging. The -% components should be scaled between 0 and 1. The space can be -% visualized geometrically as a cube. -% -% Y'PbPr, Y'CbCr, Y'DbDr, Y'UV, and Y'IQ are related to sRGB by linear -% transformations. These spaces separate a color into a grayscale -% luminance component Y and two chroma components. The valid ranges of -% the components depends on the space. -% -% HSV (Hue Saturation Value) is related to sRGB by -% H = hexagonal hue angle (0 <= H < 360), -% S = C/V (0 <= S <= 1), -% V = max(R',G',B') (0 <= V <= 1), -% where C = max(R',G',B') - min(R',G',B'). The hue angle H is computed on -% a hexagon. The space is geometrically a hexagonal cone. -% -% HSL (Hue Saturation Lightness) is related to sRGB by -% H = hexagonal hue angle (0 <= H < 360), -% S = C/(1 - |2L-1|) (0 <= S <= 1), -% L = (max(R',G',B') + min(R',G',B'))/2 (0 <= L <= 1), -% where H and C are the same as in HSV. Geometrically, the space is a -% double hexagonal cone. -% -% HSI (Hue Saturation Intensity) is related to sRGB by -% H = polar hue angle (0 <= H < 360), -% S = 1 - min(R',G',B')/I (0 <= S <= 1), -% I = (R'+G'+B')/3 (0 <= I <= 1). -% Unlike HSV and HSL, the hue angle H is computed on a circle rather than -% a hexagon. -% -% CIE XYZ is related to sRGB by inverse gamma correction followed by a -% linear transform. Other CIE color spaces are defined relative to XYZ. -% -% CIE L*a*b*, L*u*v*, and L*C*H* are nonlinear functions of XYZ. The L* -% component is designed to match closely with human perception of -% lightness. The other two components describe the chroma. -% -% CIE CAT02 LMS is the linear transformation of XYZ using the MCAT02 -% chromatic adaptation matrix. The space is designed to model the -% response of the three types of cones in the human eye, where L, M, S, -% correspond respectively to red ("long"), green ("medium"), and blue -% ("short"). - -% Pascal Getreuer 2005-2010 -% All rights reserved. -% -% Redistribution and use in source and binary forms, with or without -% modification, are permitted provided that the following conditions are -% met: -% - % * Redistributions of source code must retain the above copyright - % notice, this list of conditions and the following disclaimer. - % * Redistributions in binary form must reproduce the above copyright - % notice, this list of conditions and the following disclaimer in - % the documentation and/or other materials provided with the distribution -% -% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -% POSSIBILITY OF SUCH DAMAGE. - -%%% Input parsing %%% -if nargin < 2, error('Not enough input arguments.'); end -[SrcSpace,DestSpace] = parse(Conversion); - -if nargin == 2 - Image = varargin{1}; -elseif nargin >= 3 - Image = cat(3,varargin{:}); -else - error('Invalid number of input arguments.'); -end - -FlipDims = (size(Image,3) == 1); - -if FlipDims, Image = permute(Image,[1,3,2]); end -if ~isa(Image,'double'), Image = double(Image)/255; end -if size(Image,3) ~= 3, error('Invalid input size.'); end - -SrcT = gettransform(SrcSpace); -DestT = gettransform(DestSpace); - -if ~ischar(SrcT) && ~ischar(DestT) - % Both source and destination transforms are affine, so they - % can be composed into one affine operation - T = [DestT(:,1:3)*SrcT(:,1:3),DestT(:,1:3)*SrcT(:,4)+DestT(:,4)]; - Temp = zeros(size(Image)); - Temp(:,:,1) = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3) + T(10); - Temp(:,:,2) = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3) + T(11); - Temp(:,:,3) = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3) + T(12); - Image = Temp; -elseif ~ischar(DestT) - Image = rgb(Image,SrcSpace); - Temp = zeros(size(Image)); - Temp(:,:,1) = DestT(1)*Image(:,:,1) + DestT(4)*Image(:,:,2) + DestT(7)*Image(:,:,3) + DestT(10); - Temp(:,:,2) = DestT(2)*Image(:,:,1) + DestT(5)*Image(:,:,2) + DestT(8)*Image(:,:,3) + DestT(11); - Temp(:,:,3) = DestT(3)*Image(:,:,1) + DestT(6)*Image(:,:,2) + DestT(9)*Image(:,:,3) + DestT(12); - Image = Temp; -else - Image = feval(DestT,Image,SrcSpace); -end - -%%% Output format %%% -if nargout > 1 - varargout = {Image(:,:,1),Image(:,:,2),Image(:,:,3)}; -else - if FlipDims, Image = permute(Image,[1,3,2]); end - varargout = {Image}; -end - -return; - - -function [SrcSpace,DestSpace] = parse(Str) -% Parse conversion argument - -if ischar(Str) - Str = lower(strrep(strrep(Str,'-',''),'=','')); - k = find(Str == '>'); - - if length(k) == 1 % Interpret the form 'src->dest' - SrcSpace = Str(1:k-1); - DestSpace = Str(k+1:end); - else - k = find(Str == '<'); - - if length(k) == 1 % Interpret the form 'dest<-src' - DestSpace = Str(1:k-1); - SrcSpace = Str(k+1:end); - else - error(['Invalid conversion, ''',Str,'''.']); - end - end - - SrcSpace = alias(SrcSpace); - DestSpace = alias(DestSpace); -else - SrcSpace = 1; % No source pre-transform - DestSpace = Conversion; - if any(size(Conversion) ~= 3), error('Transformation matrix must be 3x3.'); end -end -return; - - -function Space = alias(Space) -Space = strrep(strrep(Space,'cie',''),' ',''); - -if isempty(Space) - Space = 'rgb'; -end - -switch Space -case {'ycbcr','ycc'} - Space = 'ycbcr'; -case {'hsv','hsb'} - Space = 'hsv'; -case {'hsl','hsi','hls'} - Space = 'hsl'; -case {'rgb','yuv','yiq','ydbdr','ycbcr','jpegycbcr','xyz','lab','luv','lch'} - return; -end -return; - - -function T = gettransform(Space) -% Get a colorspace transform: either a matrix describing an affine transform, -% or a string referring to a conversion subroutine -switch Space -case 'ypbpr' - T = [0.299,0.587,0.114,0;-0.1687367,-0.331264,0.5,0;0.5,-0.418688,-0.081312,0]; -case 'yuv' - % sRGB to NTSC/PAL YUV - % Wikipedia: http://en.wikipedia.org/wiki/YUV - T = [0.299,0.587,0.114,0;-0.147,-0.289,0.436,0;0.615,-0.515,-0.100,0]; -case 'ydbdr' - % sRGB to SECAM YDbDr - % Wikipedia: http://en.wikipedia.org/wiki/YDbDr - T = [0.299,0.587,0.114,0;-0.450,-0.883,1.333,0;-1.333,1.116,0.217,0]; -case 'yiq' - % sRGB in [0,1] to NTSC YIQ in [0,1];[-0.595716,0.595716];[-0.522591,0.522591]; - % Wikipedia: http://en.wikipedia.org/wiki/YIQ - T = [0.299,0.587,0.114,0;0.595716,-0.274453,-0.321263,0;0.211456,-0.522591,0.311135,0]; -case 'ycbcr' - % sRGB (range [0,1]) to ITU-R BRT.601 (CCIR 601) Y'CbCr - % Wikipedia: http://en.wikipedia.org/wiki/YCbCr - % Poynton, Equation 3, scaling of R'G'B to Y'PbPr conversion - T = [65.481,128.553,24.966,16;-37.797,-74.203,112.0,128;112.0,-93.786,-18.214,128]; -case 'jpegycbcr' - % Wikipedia: http://en.wikipedia.org/wiki/YCbCr - T = [0.299,0.587,0.114,0;-0.168736,-0.331264,0.5,0.5;0.5,-0.418688,-0.081312,0.5]*255; -case {'rgb','xyz','hsv','hsl','lab','luv','lch','cat02lms'} - T = Space; -otherwise - error(['Unknown color space, ''',Space,'''.']); -end -return; - - -function Image = rgb(Image,SrcSpace) -% Convert to sRGB from 'SrcSpace' -switch SrcSpace -case 'rgb' - return; -case 'hsv' - % Convert HSV to sRGB - Image = huetorgb((1 - Image(:,:,2)).*Image(:,:,3),Image(:,:,3),Image(:,:,1)); -case 'hsl' - % Convert HSL to sRGB - L = Image(:,:,3); - Delta = Image(:,:,2).*min(L,1-L); - Image = huetorgb(L-Delta,L+Delta,Image(:,:,1)); -case {'xyz','lab','luv','lch','cat02lms'} - % Convert to CIE XYZ - Image = xyz(Image,SrcSpace); - % Convert XYZ to RGB - T = [3.2406, -1.5372, -0.4986; -0.9689, 1.8758, 0.0415; 0.0557, -0.2040, 1.057]; - R = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3); % R - G = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3); % G - B = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3); % B - % Desaturate and rescale to constrain resulting RGB values to [0,1] - AddWhite = -min(min(min(R,G),B),0); - R = R + AddWhite; - G = G + AddWhite; - B = B + AddWhite; - % Apply gamma correction to convert linear RGB to sRGB - Image(:,:,1) = gammacorrection(R); % R' - Image(:,:,2) = gammacorrection(G); % G' - Image(:,:,3) = gammacorrection(B); % B' -otherwise % Conversion is through an affine transform - T = gettransform(SrcSpace); - temp = inv(T(:,1:3)); - T = [temp,-temp*T(:,4)]; - R = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3) + T(10); - G = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3) + T(11); - B = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3) + T(12); - Image(:,:,1) = R; - Image(:,:,2) = G; - Image(:,:,3) = B; -end - -% Clip to [0,1] -Image = min(max(Image,0),1); -return; - - -function Image = xyz(Image,SrcSpace) -% Convert to CIE XYZ from 'SrcSpace' -WhitePoint = [0.950456,1,1.088754]; - -switch SrcSpace -case 'xyz' - return; -case 'luv' - % Convert CIE L*uv to XYZ - WhitePointU = (4*WhitePoint(1))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); - WhitePointV = (9*WhitePoint(2))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); - L = Image(:,:,1); - Y = (L + 16)/116; - Y = invf(Y)*WhitePoint(2); - U = Image(:,:,2)./(13*L + 1e-6*(L==0)) + WhitePointU; - V = Image(:,:,3)./(13*L + 1e-6*(L==0)) + WhitePointV; - Image(:,:,1) = -(9*Y.*U)./((U-4).*V - U.*V); % X - Image(:,:,2) = Y; % Y - Image(:,:,3) = (9*Y - (15*V.*Y) - (V.*Image(:,:,1)))./(3*V); % Z -case {'lab','lch'} - Image = lab(Image,SrcSpace); - % Convert CIE L*ab to XYZ - fY = (Image(:,:,1) + 16)/116; - fX = fY + Image(:,:,2)/500; - fZ = fY - Image(:,:,3)/200; - Image(:,:,1) = WhitePoint(1)*invf(fX); % X - Image(:,:,2) = WhitePoint(2)*invf(fY); % Y - Image(:,:,3) = WhitePoint(3)*invf(fZ); % Z -case 'cat02lms' - % Convert CAT02 LMS to XYZ - T = inv([0.7328, 0.4296, -0.1624;-0.7036, 1.6975, 0.0061; 0.0030, 0.0136, 0.9834]); - L = Image(:,:,1); - M = Image(:,:,2); - S = Image(:,:,3); - Image(:,:,1) = T(1)*L + T(4)*M + T(7)*S; % X - Image(:,:,2) = T(2)*L + T(5)*M + T(8)*S; % Y - Image(:,:,3) = T(3)*L + T(6)*M + T(9)*S; % Z -otherwise % Convert from some gamma-corrected space - % Convert to sRGB - Image = rgb(Image,SrcSpace); - % Undo gamma correction - R = invgammacorrection(Image(:,:,1)); - G = invgammacorrection(Image(:,:,2)); - B = invgammacorrection(Image(:,:,3)); - % Convert RGB to XYZ - T = inv([3.2406, -1.5372, -0.4986; -0.9689, 1.8758, 0.0415; 0.0557, -0.2040, 1.057]); - Image(:,:,1) = T(1)*R + T(4)*G + T(7)*B; % X - Image(:,:,2) = T(2)*R + T(5)*G + T(8)*B; % Y - Image(:,:,3) = T(3)*R + T(6)*G + T(9)*B; % Z -end -return; - - -function Image = hsv(Image,SrcSpace) -% Convert to HSV -Image = rgb(Image,SrcSpace); -V = max(Image,[],3); -S = (V - min(Image,[],3))./(V + (V == 0)); -Image(:,:,1) = rgbtohue(Image); -Image(:,:,2) = S; -Image(:,:,3) = V; -return; - - -function Image = hsl(Image,SrcSpace) -% Convert to HSL -switch SrcSpace -case 'hsv' - % Convert HSV to HSL - MaxVal = Image(:,:,3); - MinVal = (1 - Image(:,:,2)).*MaxVal; - L = 0.5*(MaxVal + MinVal); - temp = min(L,1-L); - Image(:,:,2) = 0.5*(MaxVal - MinVal)./(temp + (temp == 0)); - Image(:,:,3) = L; -otherwise - Image = rgb(Image,SrcSpace); % Convert to sRGB - % Convert sRGB to HSL - MinVal = min(Image,[],3); - MaxVal = max(Image,[],3); - L = 0.5*(MaxVal + MinVal); - temp = min(L,1-L); - S = 0.5*(MaxVal - MinVal)./(temp + (temp == 0)); - Image(:,:,1) = rgbtohue(Image); - Image(:,:,2) = S; - Image(:,:,3) = L; -end -return; - - -function Image = lab(Image,SrcSpace) -% Convert to CIE L*a*b* (CIELAB) -WhitePoint = [0.950456,1,1.088754]; - -switch SrcSpace -case 'lab' - return; -case 'lch' - % Convert CIE L*CH to CIE L*ab - C = Image(:,:,2); - Image(:,:,2) = cos(Image(:,:,3)*pi/180).*C; % a* - Image(:,:,3) = sin(Image(:,:,3)*pi/180).*C; % b* -otherwise - Image = xyz(Image,SrcSpace); % Convert to XYZ - % Convert XYZ to CIE L*a*b* - X = Image(:,:,1)/WhitePoint(1); - Y = Image(:,:,2)/WhitePoint(2); - Z = Image(:,:,3)/WhitePoint(3); - fX = f(X); - fY = f(Y); - fZ = f(Z); - Image(:,:,1) = 116*fY - 16; % L* - Image(:,:,2) = 500*(fX - fY); % a* - Image(:,:,3) = 200*(fY - fZ); % b* -end -return; - - -function Image = luv(Image,SrcSpace) -% Convert to CIE L*u*v* (CIELUV) -WhitePoint = [0.950456,1,1.088754]; -WhitePointU = (4*WhitePoint(1))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); -WhitePointV = (9*WhitePoint(2))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); - -Image = xyz(Image,SrcSpace); % Convert to XYZ -Denom = Image(:,:,1) + 15*Image(:,:,2) + 3*Image(:,:,3); -U = (4*Image(:,:,1))./(Denom + (Denom == 0)); -V = (9*Image(:,:,2))./(Denom + (Denom == 0)); -Y = Image(:,:,2)/WhitePoint(2); -L = 116*f(Y) - 16; -Image(:,:,1) = L; % L* -Image(:,:,2) = 13*L.*(U - WhitePointU); % u* -Image(:,:,3) = 13*L.*(V - WhitePointV); % v* -return; - - -function Image = lch(Image,SrcSpace) -% Convert to CIE L*ch -Image = lab(Image,SrcSpace); % Convert to CIE L*ab -H = atan2(Image(:,:,3),Image(:,:,2)); -H = H*180/pi + 360*(H < 0); -Image(:,:,2) = sqrt(Image(:,:,2).^2 + Image(:,:,3).^2); % C -Image(:,:,3) = H; % H -return; - - -function Image = cat02lms(Image,SrcSpace) -% Convert to CAT02 LMS -Image = xyz(Image,SrcSpace); -T = [0.7328, 0.4296, -0.1624;-0.7036, 1.6975, 0.0061; 0.0030, 0.0136, 0.9834]; -X = Image(:,:,1); -Y = Image(:,:,2); -Z = Image(:,:,3); -Image(:,:,1) = T(1)*X + T(4)*Y + T(7)*Z; % L -Image(:,:,2) = T(2)*X + T(5)*Y + T(8)*Z; % M -Image(:,:,3) = T(3)*X + T(6)*Y + T(9)*Z; % S -return; - - -function Image = huetorgb(m0,m2,H) -% Convert HSV or HSL hue to RGB -N = size(H); -H = min(max(H(:),0),360)/60; -m0 = m0(:); -m2 = m2(:); -F = H - round(H/2)*2; -M = [m0, m0 + (m2-m0).*abs(F), m2]; -Num = length(m0); -j = [2 1 0;1 2 0;0 2 1;0 1 2;1 0 2;2 0 1;2 1 0]*Num; -k = floor(H) + 1; -Image = reshape([M(j(k,1)+(1:Num).'),M(j(k,2)+(1:Num).'),M(j(k,3)+(1:Num).')],[N,3]); -return; - - -function H = rgbtohue(Image) -% Convert RGB to HSV or HSL hue -[M,i] = sort(Image,3); -i = i(:,:,3); -Delta = M(:,:,3) - M(:,:,1); -Delta = Delta + (Delta == 0); -R = Image(:,:,1); -G = Image(:,:,2); -B = Image(:,:,3); -H = zeros(size(R)); -k = (i == 1); -H(k) = (G(k) - B(k))./Delta(k); -k = (i == 2); -H(k) = 2 + (B(k) - R(k))./Delta(k); -k = (i == 3); -H(k) = 4 + (R(k) - G(k))./Delta(k); -H = 60*H + 360*(H < 0); -H(Delta == 0) = nan; -return; - - -function Rp = gammacorrection(R) -Rp = zeros(size(R)); -i = (R <= 0.0031306684425005883); -Rp(i) = 12.92*R(i); -Rp(~i) = real(1.055*R(~i).^0.416666666666666667 - 0.055); -return; - - -function R = invgammacorrection(Rp) -R = zeros(size(Rp)); -i = (Rp <= 0.0404482362771076); -R(i) = Rp(i)/12.92; -R(~i) = real(((Rp(~i) + 0.055)/1.055).^2.4); -return; - - -function fY = f(Y) -fY = real(Y.^(1/3)); -i = (Y < 0.008856); -fY(i) = Y(i)*(841/108) + (4/29); -return; - - -function Y = invf(fY) -Y = fY.^3; -i = (Y < 0.008856); -Y(i) = (fY(i) - 4/29)*(108/841); -return; +function varargout = colorspace(Conversion,varargin) +%COLORSPACE Transform a color image between color representations. +% B = COLORSPACE(S,A) transforms the color representation of image A +% where S is a string specifying the conversion. The input array A +% should be a real full double array of size Mx3 or MxNx3. The output B +% is the same size as A. +% +% S tells the source and destination color spaces, S = 'dest<-src', or +% alternatively, S = 'src->dest'. Supported color spaces are +% +% 'RGB' sRGB IEC 61966-2-1 +% 'YCbCr' Luma + Chroma ("digitized" version of Y'PbPr) +% 'JPEG-YCbCr' Luma + Chroma space used in JFIF JPEG +% 'YDbDr' SECAM Y'DbDr Luma + Chroma +% 'YPbPr' Luma (ITU-R BT.601) + Chroma +% 'YUV' NTSC PAL Y'UV Luma + Chroma +% 'YIQ' NTSC Y'IQ Luma + Chroma +% 'HSV' or 'HSB' Hue Saturation Value/Brightness +% 'HSL' or 'HLS' Hue Saturation Luminance +% 'HSI' Hue Saturation Intensity +% 'XYZ' CIE 1931 XYZ +% 'Lab' CIE 1976 L*a*b* (CIELAB) +% 'Luv' CIE L*u*v* (CIELUV) +% 'LCH' CIE L*C*H* (CIELCH) +% 'CAT02 LMS' CIE CAT02 LMS +% +% All conversions assume 2 degree observer and D65 illuminant. +% +% Color space names are case insensitive and spaces are ignored. When +% sRGB is the source or destination, it can be omitted. For example +% 'yuv<-' is short for 'yuv<-rgb'. +% +% For sRGB, the values should be scaled between 0 and 1. Beware that +% transformations generally do not constrain colors to be "in gamut." +% Particularly, transforming from another space to sRGB may obtain +% R'G'B' values outside of the [0,1] range. So the result should be +% clamped to [0,1] before displaying: +% image(min(max(B,0),1)); % Clamp B to [0,1] and display +% +% sRGB (Red Green Blue) is the (ITU-R BT.709 gamma-corrected) standard +% red-green-blue representation of colors used in digital imaging. The +% components should be scaled between 0 and 1. The space can be +% visualized geometrically as a cube. +% +% Y'PbPr, Y'CbCr, Y'DbDr, Y'UV, and Y'IQ are related to sRGB by linear +% transformations. These spaces separate a color into a grayscale +% luminance component Y and two chroma components. The valid ranges of +% the components depends on the space. +% +% HSV (Hue Saturation Value) is related to sRGB by +% H = hexagonal hue angle (0 <= H < 360), +% S = C/V (0 <= S <= 1), +% V = max(R',G',B') (0 <= V <= 1), +% where C = max(R',G',B') - min(R',G',B'). The hue angle H is computed on +% a hexagon. The space is geometrically a hexagonal cone. +% +% HSL (Hue Saturation Lightness) is related to sRGB by +% H = hexagonal hue angle (0 <= H < 360), +% S = C/(1 - |2L-1|) (0 <= S <= 1), +% L = (max(R',G',B') + min(R',G',B'))/2 (0 <= L <= 1), +% where H and C are the same as in HSV. Geometrically, the space is a +% double hexagonal cone. +% +% HSI (Hue Saturation Intensity) is related to sRGB by +% H = polar hue angle (0 <= H < 360), +% S = 1 - min(R',G',B')/I (0 <= S <= 1), +% I = (R'+G'+B')/3 (0 <= I <= 1). +% Unlike HSV and HSL, the hue angle H is computed on a circle rather than +% a hexagon. +% +% CIE XYZ is related to sRGB by inverse gamma correction followed by a +% linear transform. Other CIE color spaces are defined relative to XYZ. +% +% CIE L*a*b*, L*u*v*, and L*C*H* are nonlinear functions of XYZ. The L* +% component is designed to match closely with human perception of +% lightness. The other two components describe the chroma. +% +% CIE CAT02 LMS is the linear transformation of XYZ using the MCAT02 +% chromatic adaptation matrix. The space is designed to model the +% response of the three types of cones in the human eye, where L, M, S, +% correspond respectively to red ("long"), green ("medium"), and blue +% ("short"). + +% Pascal Getreuer 2005-2010 +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are +% met: +% + % * Redistributions of source code must retain the above copyright + % notice, this list of conditions and the following disclaimer. + % * Redistributions in binary form must reproduce the above copyright + % notice, this list of conditions and the following disclaimer in + % the documentation and/or other materials provided with the distribution +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. + +%%% Input parsing %%% +if nargin < 2, error('Not enough input arguments.'); end +[SrcSpace,DestSpace] = parse(Conversion); + +if nargin == 2 + Image = varargin{1}; +elseif nargin >= 3 + Image = cat(3,varargin{:}); +else + error('Invalid number of input arguments.'); +end + +FlipDims = (size(Image,3) == 1); + +if FlipDims, Image = permute(Image,[1,3,2]); end +if ~isa(Image,'double'), Image = double(Image)/255; end +if size(Image,3) ~= 3, error('Invalid input size.'); end + +SrcT = gettransform(SrcSpace); +DestT = gettransform(DestSpace); + +if ~ischar(SrcT) && ~ischar(DestT) + % Both source and destination transforms are affine, so they + % can be composed into one affine operation + T = [DestT(:,1:3)*SrcT(:,1:3),DestT(:,1:3)*SrcT(:,4)+DestT(:,4)]; + Temp = zeros(size(Image)); + Temp(:,:,1) = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3) + T(10); + Temp(:,:,2) = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3) + T(11); + Temp(:,:,3) = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3) + T(12); + Image = Temp; +elseif ~ischar(DestT) + Image = rgb(Image,SrcSpace); + Temp = zeros(size(Image)); + Temp(:,:,1) = DestT(1)*Image(:,:,1) + DestT(4)*Image(:,:,2) + DestT(7)*Image(:,:,3) + DestT(10); + Temp(:,:,2) = DestT(2)*Image(:,:,1) + DestT(5)*Image(:,:,2) + DestT(8)*Image(:,:,3) + DestT(11); + Temp(:,:,3) = DestT(3)*Image(:,:,1) + DestT(6)*Image(:,:,2) + DestT(9)*Image(:,:,3) + DestT(12); + Image = Temp; +else + Image = feval(DestT,Image,SrcSpace); +end + +%%% Output format %%% +if nargout > 1 + varargout = {Image(:,:,1),Image(:,:,2),Image(:,:,3)}; +else + if FlipDims, Image = permute(Image,[1,3,2]); end + varargout = {Image}; +end + +return; + + +function [SrcSpace,DestSpace] = parse(Str) +% Parse conversion argument + +if ischar(Str) + Str = lower(strrep(strrep(Str,'-',''),'=','')); + k = find(Str == '>'); + + if length(k) == 1 % Interpret the form 'src->dest' + SrcSpace = Str(1:k-1); + DestSpace = Str(k+1:end); + else + k = find(Str == '<'); + + if length(k) == 1 % Interpret the form 'dest<-src' + DestSpace = Str(1:k-1); + SrcSpace = Str(k+1:end); + else + error(['Invalid conversion, ''',Str,'''.']); + end + end + + SrcSpace = alias(SrcSpace); + DestSpace = alias(DestSpace); +else + SrcSpace = 1; % No source pre-transform + DestSpace = Conversion; + if any(size(Conversion) ~= 3), error('Transformation matrix must be 3x3.'); end +end +return; + + +function Space = alias(Space) +Space = strrep(strrep(Space,'cie',''),' ',''); + +if isempty(Space) + Space = 'rgb'; +end + +switch Space +case {'ycbcr','ycc'} + Space = 'ycbcr'; +case {'hsv','hsb'} + Space = 'hsv'; +case {'hsl','hsi','hls'} + Space = 'hsl'; +case {'rgb','yuv','yiq','ydbdr','ycbcr','jpegycbcr','xyz','lab','luv','lch'} + return; +end +return; + + +function T = gettransform(Space) +% Get a colorspace transform: either a matrix describing an affine transform, +% or a string referring to a conversion subroutine +switch Space +case 'ypbpr' + T = [0.299,0.587,0.114,0;-0.1687367,-0.331264,0.5,0;0.5,-0.418688,-0.081312,0]; +case 'yuv' + % sRGB to NTSC/PAL YUV + % Wikipedia: http://en.wikipedia.org/wiki/YUV + T = [0.299,0.587,0.114,0;-0.147,-0.289,0.436,0;0.615,-0.515,-0.100,0]; +case 'ydbdr' + % sRGB to SECAM YDbDr + % Wikipedia: http://en.wikipedia.org/wiki/YDbDr + T = [0.299,0.587,0.114,0;-0.450,-0.883,1.333,0;-1.333,1.116,0.217,0]; +case 'yiq' + % sRGB in [0,1] to NTSC YIQ in [0,1];[-0.595716,0.595716];[-0.522591,0.522591]; + % Wikipedia: http://en.wikipedia.org/wiki/YIQ + T = [0.299,0.587,0.114,0;0.595716,-0.274453,-0.321263,0;0.211456,-0.522591,0.311135,0]; +case 'ycbcr' + % sRGB (range [0,1]) to ITU-R BRT.601 (CCIR 601) Y'CbCr + % Wikipedia: http://en.wikipedia.org/wiki/YCbCr + % Poynton, Equation 3, scaling of R'G'B to Y'PbPr conversion + T = [65.481,128.553,24.966,16;-37.797,-74.203,112.0,128;112.0,-93.786,-18.214,128]; +case 'jpegycbcr' + % Wikipedia: http://en.wikipedia.org/wiki/YCbCr + T = [0.299,0.587,0.114,0;-0.168736,-0.331264,0.5,0.5;0.5,-0.418688,-0.081312,0.5]*255; +case {'rgb','xyz','hsv','hsl','lab','luv','lch','cat02lms'} + T = Space; +otherwise + error(['Unknown color space, ''',Space,'''.']); +end +return; + + +function Image = rgb(Image,SrcSpace) +% Convert to sRGB from 'SrcSpace' +switch SrcSpace +case 'rgb' + return; +case 'hsv' + % Convert HSV to sRGB + Image = huetorgb((1 - Image(:,:,2)).*Image(:,:,3),Image(:,:,3),Image(:,:,1)); +case 'hsl' + % Convert HSL to sRGB + L = Image(:,:,3); + Delta = Image(:,:,2).*min(L,1-L); + Image = huetorgb(L-Delta,L+Delta,Image(:,:,1)); +case {'xyz','lab','luv','lch','cat02lms'} + % Convert to CIE XYZ + Image = xyz(Image,SrcSpace); + % Convert XYZ to RGB + T = [3.2406, -1.5372, -0.4986; -0.9689, 1.8758, 0.0415; 0.0557, -0.2040, 1.057]; + R = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3); % R + G = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3); % G + B = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3); % B + % Desaturate and rescale to constrain resulting RGB values to [0,1] + AddWhite = -min(min(min(R,G),B),0); + R = R + AddWhite; + G = G + AddWhite; + B = B + AddWhite; + % Apply gamma correction to convert linear RGB to sRGB + Image(:,:,1) = gammacorrection(R); % R' + Image(:,:,2) = gammacorrection(G); % G' + Image(:,:,3) = gammacorrection(B); % B' +otherwise % Conversion is through an affine transform + T = gettransform(SrcSpace); + temp = inv(T(:,1:3)); + T = [temp,-temp*T(:,4)]; + R = T(1)*Image(:,:,1) + T(4)*Image(:,:,2) + T(7)*Image(:,:,3) + T(10); + G = T(2)*Image(:,:,1) + T(5)*Image(:,:,2) + T(8)*Image(:,:,3) + T(11); + B = T(3)*Image(:,:,1) + T(6)*Image(:,:,2) + T(9)*Image(:,:,3) + T(12); + Image(:,:,1) = R; + Image(:,:,2) = G; + Image(:,:,3) = B; +end + +% Clip to [0,1] +Image = min(max(Image,0),1); +return; + + +function Image = xyz(Image,SrcSpace) +% Convert to CIE XYZ from 'SrcSpace' +WhitePoint = [0.950456,1,1.088754]; + +switch SrcSpace +case 'xyz' + return; +case 'luv' + % Convert CIE L*uv to XYZ + WhitePointU = (4*WhitePoint(1))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); + WhitePointV = (9*WhitePoint(2))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); + L = Image(:,:,1); + Y = (L + 16)/116; + Y = invf(Y)*WhitePoint(2); + U = Image(:,:,2)./(13*L + 1e-6*(L==0)) + WhitePointU; + V = Image(:,:,3)./(13*L + 1e-6*(L==0)) + WhitePointV; + Image(:,:,1) = -(9*Y.*U)./((U-4).*V - U.*V); % X + Image(:,:,2) = Y; % Y + Image(:,:,3) = (9*Y - (15*V.*Y) - (V.*Image(:,:,1)))./(3*V); % Z +case {'lab','lch'} + Image = lab(Image,SrcSpace); + % Convert CIE L*ab to XYZ + fY = (Image(:,:,1) + 16)/116; + fX = fY + Image(:,:,2)/500; + fZ = fY - Image(:,:,3)/200; + Image(:,:,1) = WhitePoint(1)*invf(fX); % X + Image(:,:,2) = WhitePoint(2)*invf(fY); % Y + Image(:,:,3) = WhitePoint(3)*invf(fZ); % Z +case 'cat02lms' + % Convert CAT02 LMS to XYZ + T = inv([0.7328, 0.4296, -0.1624;-0.7036, 1.6975, 0.0061; 0.0030, 0.0136, 0.9834]); + L = Image(:,:,1); + M = Image(:,:,2); + S = Image(:,:,3); + Image(:,:,1) = T(1)*L + T(4)*M + T(7)*S; % X + Image(:,:,2) = T(2)*L + T(5)*M + T(8)*S; % Y + Image(:,:,3) = T(3)*L + T(6)*M + T(9)*S; % Z +otherwise % Convert from some gamma-corrected space + % Convert to sRGB + Image = rgb(Image,SrcSpace); + % Undo gamma correction + R = invgammacorrection(Image(:,:,1)); + G = invgammacorrection(Image(:,:,2)); + B = invgammacorrection(Image(:,:,3)); + % Convert RGB to XYZ + T = inv([3.2406, -1.5372, -0.4986; -0.9689, 1.8758, 0.0415; 0.0557, -0.2040, 1.057]); + Image(:,:,1) = T(1)*R + T(4)*G + T(7)*B; % X + Image(:,:,2) = T(2)*R + T(5)*G + T(8)*B; % Y + Image(:,:,3) = T(3)*R + T(6)*G + T(9)*B; % Z +end +return; + + +function Image = hsv(Image,SrcSpace) +% Convert to HSV +Image = rgb(Image,SrcSpace); +V = max(Image,[],3); +S = (V - min(Image,[],3))./(V + (V == 0)); +Image(:,:,1) = rgbtohue(Image); +Image(:,:,2) = S; +Image(:,:,3) = V; +return; + + +function Image = hsl(Image,SrcSpace) +% Convert to HSL +switch SrcSpace +case 'hsv' + % Convert HSV to HSL + MaxVal = Image(:,:,3); + MinVal = (1 - Image(:,:,2)).*MaxVal; + L = 0.5*(MaxVal + MinVal); + temp = min(L,1-L); + Image(:,:,2) = 0.5*(MaxVal - MinVal)./(temp + (temp == 0)); + Image(:,:,3) = L; +otherwise + Image = rgb(Image,SrcSpace); % Convert to sRGB + % Convert sRGB to HSL + MinVal = min(Image,[],3); + MaxVal = max(Image,[],3); + L = 0.5*(MaxVal + MinVal); + temp = min(L,1-L); + S = 0.5*(MaxVal - MinVal)./(temp + (temp == 0)); + Image(:,:,1) = rgbtohue(Image); + Image(:,:,2) = S; + Image(:,:,3) = L; +end +return; + + +function Image = lab(Image,SrcSpace) +% Convert to CIE L*a*b* (CIELAB) +WhitePoint = [0.950456,1,1.088754]; + +switch SrcSpace +case 'lab' + return; +case 'lch' + % Convert CIE L*CH to CIE L*ab + C = Image(:,:,2); + Image(:,:,2) = cos(Image(:,:,3)*pi/180).*C; % a* + Image(:,:,3) = sin(Image(:,:,3)*pi/180).*C; % b* +otherwise + Image = xyz(Image,SrcSpace); % Convert to XYZ + % Convert XYZ to CIE L*a*b* + X = Image(:,:,1)/WhitePoint(1); + Y = Image(:,:,2)/WhitePoint(2); + Z = Image(:,:,3)/WhitePoint(3); + fX = f(X); + fY = f(Y); + fZ = f(Z); + Image(:,:,1) = 116*fY - 16; % L* + Image(:,:,2) = 500*(fX - fY); % a* + Image(:,:,3) = 200*(fY - fZ); % b* +end +return; + + +function Image = luv(Image,SrcSpace) +% Convert to CIE L*u*v* (CIELUV) +WhitePoint = [0.950456,1,1.088754]; +WhitePointU = (4*WhitePoint(1))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); +WhitePointV = (9*WhitePoint(2))./(WhitePoint(1) + 15*WhitePoint(2) + 3*WhitePoint(3)); + +Image = xyz(Image,SrcSpace); % Convert to XYZ +Denom = Image(:,:,1) + 15*Image(:,:,2) + 3*Image(:,:,3); +U = (4*Image(:,:,1))./(Denom + (Denom == 0)); +V = (9*Image(:,:,2))./(Denom + (Denom == 0)); +Y = Image(:,:,2)/WhitePoint(2); +L = 116*f(Y) - 16; +Image(:,:,1) = L; % L* +Image(:,:,2) = 13*L.*(U - WhitePointU); % u* +Image(:,:,3) = 13*L.*(V - WhitePointV); % v* +return; + + +function Image = lch(Image,SrcSpace) +% Convert to CIE L*ch +Image = lab(Image,SrcSpace); % Convert to CIE L*ab +H = atan2(Image(:,:,3),Image(:,:,2)); +H = H*180/pi + 360*(H < 0); +Image(:,:,2) = sqrt(Image(:,:,2).^2 + Image(:,:,3).^2); % C +Image(:,:,3) = H; % H +return; + + +function Image = cat02lms(Image,SrcSpace) +% Convert to CAT02 LMS +Image = xyz(Image,SrcSpace); +T = [0.7328, 0.4296, -0.1624;-0.7036, 1.6975, 0.0061; 0.0030, 0.0136, 0.9834]; +X = Image(:,:,1); +Y = Image(:,:,2); +Z = Image(:,:,3); +Image(:,:,1) = T(1)*X + T(4)*Y + T(7)*Z; % L +Image(:,:,2) = T(2)*X + T(5)*Y + T(8)*Z; % M +Image(:,:,3) = T(3)*X + T(6)*Y + T(9)*Z; % S +return; + + +function Image = huetorgb(m0,m2,H) +% Convert HSV or HSL hue to RGB +N = size(H); +H = min(max(H(:),0),360)/60; +m0 = m0(:); +m2 = m2(:); +F = H - round(H/2)*2; +M = [m0, m0 + (m2-m0).*abs(F), m2]; +Num = length(m0); +j = [2 1 0;1 2 0;0 2 1;0 1 2;1 0 2;2 0 1;2 1 0]*Num; +k = floor(H) + 1; +Image = reshape([M(j(k,1)+(1:Num).'),M(j(k,2)+(1:Num).'),M(j(k,3)+(1:Num).')],[N,3]); +return; + + +function H = rgbtohue(Image) +% Convert RGB to HSV or HSL hue +[M,i] = sort(Image,3); +i = i(:,:,3); +Delta = M(:,:,3) - M(:,:,1); +Delta = Delta + (Delta == 0); +R = Image(:,:,1); +G = Image(:,:,2); +B = Image(:,:,3); +H = zeros(size(R)); +k = (i == 1); +H(k) = (G(k) - B(k))./Delta(k); +k = (i == 2); +H(k) = 2 + (B(k) - R(k))./Delta(k); +k = (i == 3); +H(k) = 4 + (R(k) - G(k))./Delta(k); +H = 60*H + 360*(H < 0); +H(Delta == 0) = nan; +return; + + +function Rp = gammacorrection(R) +Rp = zeros(size(R)); +i = (R <= 0.0031306684425005883); +Rp(i) = 12.92*R(i); +Rp(~i) = real(1.055*R(~i).^0.416666666666666667 - 0.055); +return; + + +function R = invgammacorrection(Rp) +R = zeros(size(Rp)); +i = (Rp <= 0.0404482362771076); +R(i) = Rp(i)/12.92; +R(~i) = real(((Rp(~i) + 0.055)/1.055).^2.4); +return; + + +function fY = f(Y) +fY = real(Y.^(1/3)); +i = (Y < 0.008856); +fY(i) = Y(i)*(841/108) + (4/29); +return; + + +function Y = invf(fY) +Y = fY.^3; +i = (Y < 0.008856); +Y(i) = (fY(i) - 4/29)*(108/841); +return; diff --git a/matlab/utilities/graphics/distinguishable_colors.m b/matlab/utilities/graphics/distinguishable_colors.m index ab88e5b19..0b9fb7a33 100644 --- a/matlab/utilities/graphics/distinguishable_colors.m +++ b/matlab/utilities/graphics/distinguishable_colors.m @@ -1,176 +1,176 @@ -function colors = distinguishable_colors(n_colors,bg,func) -% DISTINGUISHABLE_COLORS: pick colors that are maximally perceptually distinct -% -% When plotting a set of lines, you may want to distinguish them by color. -% By default, Matlab chooses a small set of colors and cycles among them, -% and so if you have more than a few lines there will be confusion about -% which line is which. To fix this problem, one would want to be able to -% pick a much larger set of distinct colors, where the number of colors -% equals or exceeds the number of lines you want to plot. Because our -% ability to distinguish among colors has limits, one should choose these -% colors to be "maximally perceptually distinguishable." -% -% This function generates a set of colors which are distinguishable -% by reference to the "Lab" color space, which more closely matches -% human color perception than RGB. Given an initial large list of possible -% colors, it iteratively chooses the entry in the list that is farthest (in -% Lab space) from all previously-chosen entries. While this "greedy" -% algorithm does not yield a global maximum, it is simple and efficient. -% Moreover, the sequence of colors is consistent no matter how many you -% request, which facilitates the users' ability to learn the color order -% and avoids major changes in the appearance of plots when adding or -% removing lines. -% -% Syntax: -% colors = distinguishable_colors(n_colors) -% Specify the number of colors you want as a scalar, n_colors. This will -% generate an n_colors-by-3 matrix, each row representing an RGB -% color triple. If you don't precisely know how many you will need in -% advance, there is no harm (other than execution time) in specifying -% slightly more than you think you will need. -% -% colors = distinguishable_colors(n_colors,bg) -% This syntax allows you to specify the background color, to make sure that -% your colors are also distinguishable from the background. Default value -% is white. bg may be specified as an RGB triple or as one of the standard -% "ColorSpec" strings. You can even specify multiple colors: -% bg = {'w','k'} -% or -% bg = [1 1 1; 0 0 0] -% will only produce colors that are distinguishable from both white and -% black. -% -% colors = distinguishable_colors(n_colors,bg,rgb2labfunc) -% By default, distinguishable_colors uses the image processing toolbox's -% color conversion functions makecform and applycform. Alternatively, you -% can supply your own color conversion function. -% -% Example: -% c = distinguishable_colors(25); -% figure -% image(reshape(c,[1 size(c)])) -% -% Example using the file exchange's 'colorspace': -% func = @(x) colorspace('RGB->Lab',x); -% c = distinguishable_colors(25,'w',func); - -% Copyright 2010-2011 by Timothy E. Holy -% All rights reserved. -% -% Redistribution and use in source and binary forms, with or without -% modification, are permitted provided that the following conditions are -% met: -% - % * Redistributions of source code must retain the above copyright - % notice, this list of conditions and the following disclaimer. - % * Redistributions in binary form must reproduce the above copyright - % notice, this list of conditions and the following disclaimer in - % the documentation and/or other materials provided with the distribution -% -% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -% POSSIBILITY OF SUCH DAMAGE. - - -% Parse the inputs -if (nargin < 2) - bg = [1 1 1]; % default white background -else - if iscell(bg) - % User specified a list of colors as a cell aray - bgc = bg; - for i = 1:length(bgc) - bgc{i} = parsecolor(bgc{i}); - end - bg = cat(1,bgc{:}); - else - % User specified a numeric array of colors (n-by-3) - bg = parsecolor(bg); - end -end - -% Generate a sizable number of RGB triples. This represents our space of -% possible choices. By starting in RGB space, we ensure that all of the -% colors can be generated by the monitor. -n_grid = 30; % number of grid divisions along each axis in RGB space -x = linspace(0,1,n_grid); -[R,G,B] = ndgrid(x,x,x); -rgb = [R(:) G(:) B(:)]; -if (n_colors > size(rgb,1)/3) - error('You can''t readily distinguish that many colors'); -end - -% Convert to Lab color space, which more closely represents human -% perception -if (nargin > 2) - lab = func(rgb); - bglab = func(bg); -else - C = makecform('srgb2lab'); - lab = applycform(rgb,C); - bglab = applycform(bg,C); -end - -% If the user specified multiple background colors, compute distances -% from the candidate colors to the background colors -mindist2 = inf(size(rgb,1),1); -for i = 1:size(bglab,1)-1 - dX = bsxfun(@minus,lab,bglab(i,:)); % displacement all colors from bg - dist2 = sum(dX.^2,2); % square distance - mindist2 = min(dist2,mindist2); % dist2 to closest previously-chosen color -end - -% Iteratively pick the color that maximizes the distance to the nearest -% already-picked color -colors = zeros(n_colors,3); -lastlab = bglab(end,:); % initialize by making the "previous" color equal to background -for i = 1:n_colors - dX = bsxfun(@minus,lab,lastlab); % displacement of last from all colors on list - dist2 = sum(dX.^2,2); % square distance - mindist2 = min(dist2,mindist2); % dist2 to closest previously-chosen color - [~,index] = max(mindist2); % find the entry farthest from all previously-chosen colors - colors(i,:) = rgb(index,:); % save for output - lastlab = lab(index,:); % prepare for next iteration -end -end - -function c = parsecolor(s) -if ischar(s) - c = colorstr2rgb(s); -elseif isnumeric(s) && size(s,2) == 3 - c = s; -else - error('MATLAB:InvalidColorSpec','Color specification cannot be parsed.'); -end -end - -function c = colorstr2rgb(c) -% Convert a color string to an RGB value. -% This is cribbed from Matlab's whitebg function. -% Why don't they make this a stand-alone function? -rgbspec = [1 0 0;0 1 0;0 0 1;1 1 1;0 1 1;1 0 1;1 1 0;0 0 0]; -cspec = 'rgbwcmyk'; -k = find(cspec==c(1)); -if isempty(k) - error('MATLAB:InvalidColorString','Unknown color string.'); -end -if k~=3 || length(c)==1, - c = rgbspec(k,:); -elseif length(c)>2, - if strcmpi(c(1:3),'bla') - c = [0 0 0]; - elseif strcmpi(c(1:3),'blu') - c = [0 0 1]; - else - error('MATLAB:UnknownColorString', 'Unknown color string.'); - end -end -end +function colors = distinguishable_colors(n_colors,bg,func) +% DISTINGUISHABLE_COLORS: pick colors that are maximally perceptually distinct +% +% When plotting a set of lines, you may want to distinguish them by color. +% By default, Matlab chooses a small set of colors and cycles among them, +% and so if you have more than a few lines there will be confusion about +% which line is which. To fix this problem, one would want to be able to +% pick a much larger set of distinct colors, where the number of colors +% equals or exceeds the number of lines you want to plot. Because our +% ability to distinguish among colors has limits, one should choose these +% colors to be "maximally perceptually distinguishable." +% +% This function generates a set of colors which are distinguishable +% by reference to the "Lab" color space, which more closely matches +% human color perception than RGB. Given an initial large list of possible +% colors, it iteratively chooses the entry in the list that is farthest (in +% Lab space) from all previously-chosen entries. While this "greedy" +% algorithm does not yield a global maximum, it is simple and efficient. +% Moreover, the sequence of colors is consistent no matter how many you +% request, which facilitates the users' ability to learn the color order +% and avoids major changes in the appearance of plots when adding or +% removing lines. +% +% Syntax: +% colors = distinguishable_colors(n_colors) +% Specify the number of colors you want as a scalar, n_colors. This will +% generate an n_colors-by-3 matrix, each row representing an RGB +% color triple. If you don't precisely know how many you will need in +% advance, there is no harm (other than execution time) in specifying +% slightly more than you think you will need. +% +% colors = distinguishable_colors(n_colors,bg) +% This syntax allows you to specify the background color, to make sure that +% your colors are also distinguishable from the background. Default value +% is white. bg may be specified as an RGB triple or as one of the standard +% "ColorSpec" strings. You can even specify multiple colors: +% bg = {'w','k'} +% or +% bg = [1 1 1; 0 0 0] +% will only produce colors that are distinguishable from both white and +% black. +% +% colors = distinguishable_colors(n_colors,bg,rgb2labfunc) +% By default, distinguishable_colors uses the image processing toolbox's +% color conversion functions makecform and applycform. Alternatively, you +% can supply your own color conversion function. +% +% Example: +% c = distinguishable_colors(25); +% figure +% image(reshape(c,[1 size(c)])) +% +% Example using the file exchange's 'colorspace': +% func = @(x) colorspace('RGB->Lab',x); +% c = distinguishable_colors(25,'w',func); + +% Copyright 2010-2011 by Timothy E. Holy +% All rights reserved. +% +% Redistribution and use in source and binary forms, with or without +% modification, are permitted provided that the following conditions are +% met: +% + % * Redistributions of source code must retain the above copyright + % notice, this list of conditions and the following disclaimer. + % * Redistributions in binary form must reproduce the above copyright + % notice, this list of conditions and the following disclaimer in + % the documentation and/or other materials provided with the distribution +% +% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +% AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +% IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +% ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +% LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +% CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +% SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +% INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +% CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +% ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +% POSSIBILITY OF SUCH DAMAGE. + + +% Parse the inputs +if (nargin < 2) + bg = [1 1 1]; % default white background +else + if iscell(bg) + % User specified a list of colors as a cell aray + bgc = bg; + for i = 1:length(bgc) + bgc{i} = parsecolor(bgc{i}); + end + bg = cat(1,bgc{:}); + else + % User specified a numeric array of colors (n-by-3) + bg = parsecolor(bg); + end +end + +% Generate a sizable number of RGB triples. This represents our space of +% possible choices. By starting in RGB space, we ensure that all of the +% colors can be generated by the monitor. +n_grid = 30; % number of grid divisions along each axis in RGB space +x = linspace(0,1,n_grid); +[R,G,B] = ndgrid(x,x,x); +rgb = [R(:) G(:) B(:)]; +if (n_colors > size(rgb,1)/3) + error('You can''t readily distinguish that many colors'); +end + +% Convert to Lab color space, which more closely represents human +% perception +if (nargin > 2) + lab = func(rgb); + bglab = func(bg); +else + C = makecform('srgb2lab'); + lab = applycform(rgb,C); + bglab = applycform(bg,C); +end + +% If the user specified multiple background colors, compute distances +% from the candidate colors to the background colors +mindist2 = inf(size(rgb,1),1); +for i = 1:size(bglab,1)-1 + dX = bsxfun(@minus,lab,bglab(i,:)); % displacement all colors from bg + dist2 = sum(dX.^2,2); % square distance + mindist2 = min(dist2,mindist2); % dist2 to closest previously-chosen color +end + +% Iteratively pick the color that maximizes the distance to the nearest +% already-picked color +colors = zeros(n_colors,3); +lastlab = bglab(end,:); % initialize by making the "previous" color equal to background +for i = 1:n_colors + dX = bsxfun(@minus,lab,lastlab); % displacement of last from all colors on list + dist2 = sum(dX.^2,2); % square distance + mindist2 = min(dist2,mindist2); % dist2 to closest previously-chosen color + [~,index] = max(mindist2); % find the entry farthest from all previously-chosen colors + colors(i,:) = rgb(index,:); % save for output + lastlab = lab(index,:); % prepare for next iteration +end +end + +function c = parsecolor(s) +if ischar(s) + c = colorstr2rgb(s); +elseif isnumeric(s) && size(s,2) == 3 + c = s; +else + error('MATLAB:InvalidColorSpec','Color specification cannot be parsed.'); +end +end + +function c = colorstr2rgb(c) +% Convert a color string to an RGB value. +% This is cribbed from Matlab's whitebg function. +% Why don't they make this a stand-alone function? +rgbspec = [1 0 0;0 1 0;0 0 1;1 1 1;0 1 1;1 0 1;1 1 0;0 0 0]; +cspec = 'rgbwcmyk'; +k = find(cspec==c(1)); +if isempty(k) + error('MATLAB:InvalidColorString','Unknown color string.'); +end +if k~=3 || length(c)==1, + c = rgbspec(k,:); +elseif length(c)>2, + if strcmpi(c(1:3),'bla') + c = [0 0 0]; + elseif strcmpi(c(1:3),'blu') + c = [0 0 1]; + else + error('MATLAB:UnknownColorString', 'Unknown color string.'); + end +end +end