changeset 663:d261283837b6

wavelength2rgb: major rewrite of function * accept ND dimensional matrix as input (instead of scalar values only) * use desired output class instead of max_intensity value * add demo * updated documentation * bug fix when adjusting intensity near the visible wavelenths limit
author carandraug
date Sat, 03 Nov 2012 20:55:57 +0000
parents a0ec22eef32c
children f26be4b599b5
files inst/wavelength2rgb.m
diffstat 1 files changed, 132 insertions(+), 75 deletions(-) [+]
line wrap: on
line diff
--- a/inst/wavelength2rgb.m
+++ b/inst/wavelength2rgb.m
@@ -15,104 +15,161 @@
 ## this program; if not, see <http://www.gnu.org/licenses/>.
 
 ## -*- texinfo -*-
-## @deftypefn{Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength})
-## @deftypefnx{Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength}, @var{intensity_max})
-## @deftypefnx{Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength}, @var{intensity_max}, @var{gamma})
+## @deftypefn  {Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength})
+## @deftypefnx {Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength}, @var{class})
+## @deftypefnx {Function File} {@var{rgb} =} wavelength2rgb (@var{wavelength}, @var{class}, @var{gamma})
 ## Convert wavelength in nm into an RGB value set.
 ##
-## Output:
-## @itemize @bullet
-## @item @var{rgb}: value set on scale of 1:@var{intensity_max}
-## @end itemize
+## Given a N-dimensional matrix @var{wavelength} with color values in nm, returns
+## a RGB image with N+3 dimensions.
 ##
-## Input:
-## Output:
-## @itemize @bullet
-## @item @var{wavelength}: wavelength of input light value in nm. Must be a positive
-## numeric scalar.
-## @item @var{intensity_max}: max of integer scale to output values. Defaults to 255.
-## @item @var{gamma}: Controls luminance. Must be a value between 0 and 1. Defaults to 0.8.
-## @end itemize
+## @group
+## @example
+## wavelength2rgb (400)
+##     @result{} [0.51222 0.00000 0.70849]
+##
+## wavelength2rgb ([400 410])
+##     @result{(:,:,1)} 0.51222   0.49242
+##     @result{(:,:,2)} 0         0
+##     @result{(:,:,3)} 0.70849   0.85736
+## @end example
+## @end group
 ##
+## The @var{rgb} class can be specified with @var{class}.  Possible values are
+## double (default), single, uint8, uint16, and int16.
+##
+## @group
 ## @example
-## X = 350:0.5:800;
-## Y = exp (log (X.^3));   # arbitrary
-## figure, plot(X, Y)
-## stepSize = 0.5 * max (diff (X));
-## for n = 1:numel(Y)
-##   RGB = wavelength2rgb (X(n), 1);
-##   if (sum (RGB) > 0 )   # ie don't fill black
-##     hold on, area( (X(n)-stepSize):(stepSize/1):(X(n)+stepSize), ...
-##                   repmat( Y(n), 1, numel((X(n)-stepSize):(stepSize/1):(X(n)+stepSize)) ), ...
-##                   'EdgeColor', RGB, 'FaceColor', RGB ); hold off
-##   endif
-## endfor
+## wavelength2rgb (400)
+##     @result{} 0.51222  0.00000  0.70849
+##
+## wavelength2rgb (400, "uint8")
+##     @result{} 131    0  181
 ## @end example
+## @end group
+##
+## The luminance of colors can be adjusted with @var{gamma} which must a scalar
+## value in the range [0 1]. Defaults to 0.8.
 ##
 ## Reference:
 ## @itemize @bullet
 ## @item @uref{http://stackoverflow.com/questions/2374959/algorithm-to-convert-any-positive-integer-to-an-rgb-value}
 ## @item @uref{http://www.midnightkite.com/color.html} per Dan Bruton
-## @end itemize
+## @end i temize
 ## @end deftypefn
 
-function rgb = wavelength2rgb (wavelength, intensity_max = 255, gamma = 0.8)
+function rgb = wavelength2rgb (wavelength, out_class = "double", gamma = 0.8)
 
   if (nargin < 1 || nargin > 3)
     print_usage;
-  elseif (!isnumeric (wavelength) || !isscalar (wavelength) || wavelength <= 0)
-    error ("wavelength must a positive numeric scalar");
-  elseif (!isnumeric (intensity_max) || !isscalar (intensity_max) || intensity_max <= 0)
-    error ("intensity_max must a positive numeric scalar");
+  elseif (!isnumeric (wavelength) || any (wavelength <= 0))
+    error ("wavelength2rgb: wavelength must a positive numeric");
+  elseif (!ischar (out_class) || all (!strcmpi (out_class, {"single", "double", "uint8", "uint16", "int16"})))
+    error ("wavelength2rgb: unsupported class `%s'", char (out_class));
   elseif (!isnumeric (gamma) || !isscalar (gamma) || gamma > 1 || gamma < 0)
-    error ("gamma must a numeric scalar between 1 and 0");
+    error ("wavelength2rgb: gamma must a numeric scalar between 1 and 0");
   endif
 
-  rgb = zeros (3, numel (wavelength));  # initialize rgb, each rgb set stored by column
-
-  ## set the factor
-  if ( wavelength >= 380 && wavelength < 420 )
-     factor = 0.3 + 0.7*(wavelength - 380) / (420 - 380);
-  elseif ( wavelength >= 420 && wavelength < 701 )
-     factor = 1;
-  elseif ( wavelength >= 420 && wavelength < 701 )
-     factor = 0.3 + 0.7*(780 - wavelength) / (780 - 700);
+  ## initialize rgb. One extra dimension of size 3 for RGB. Later on, we will
+  ## use ndims and when input is a scalar, ndims still returns 2 which means
+  ## output would be 1x1x3. Check this and adjust later on
+  if (isscalar (wavelength))
+    rgb = zeros (1, 3);
+    size_adjust = 1;
   else
-     factor = 0;
+    rgb = zeros ([size(wavelength), 3]);
+    size_adjust = 0;
   endif
 
-  ## initialize rgb
-  if ( wavelength >= 380 && wavelength < 440 )
-     rgb(1) = -(wavelength - 440) / (440 - 380);
-     rgb(2) = 0.0;
-     rgb(3) = 1.0;
-  elseif ( wavelength >= 440 && wavelength < 490 )
-     rgb(1) = 0.0;
-     rgb(2) = (wavelength - 440) / (490 - 440);
-     rgb(3) = 1.0;
-  elseif ( wavelength >= 490 && wavelength < 510 )
-     rgb(1) = 0.0;
-     rgb(2) = 1.0;
-     rgb(3) = -(wavelength - 510) / (510 - 490);
-  elseif ( wavelength >= 510 && wavelength < 580 )
-     rgb(1) = (wavelength - 510) / (580 - 510);
-     rgb(2) = 1.0;
-     rgb(3) = 0.0;
-  elseif ( wavelength >= 580 && wavelength < 645 )
-     rgb(1) = 1.0;
-     rgb(2) = -(wavelength - 645) / (645 - 580);
-     rgb(3) = 0.0;
-  elseif ( wavelength >= 645 && wavelength < 781 )
-     rgb(1) = 1.0;
-     rgb(2) = 0.0;
-     rgb(3) = 0.0;
-  else
-     rgb(1) = 0.0;
-     rgb(2) = 0.0;
-     rgb(3) = 0.0;
-  endif
+  ## this RGBmask's will be used for broadcasting later
+  Rmask = Gmask = Bmask = false ([ones(1, ndims (wavelength) - size_adjust) 3]);
+  Rmask(1)  = true;
+  Gmask(2)  = true;
+  Bmask(3)  = true;
+
+  ## for each group of wavelengths we calculate the mask, expand for the 3
+  ## channels and use Rmask, Gmask and Bmask with broadcasting to select the
+  ## right one. Will skip some channels since their values would be zero and
+  ## we already initialized the matrix with zeros()
+  get_rgb_mask = @(mask) repmat (mask, [ones(1, ndims (mask) - size_adjust) 3]);
+
+  ## FIXME when warning for broadcasting is turned off by default, this
+  ## unwind_protect block could be removed
+  bc_warn = warning ("query", "Octave:broadcast");
+  unwind_protect
+    warning ("off", "Octave:broadcast");
+    mask    = wavelength >= 380 & wavelength < 440;
+    rgbmask = get_rgb_mask (mask);
+    rgb(rgbmask & Rmask) = -(wavelength(mask) - 440) / 60; # 60 comes from 440-380
+    ## skiping green channel (values of zero)
+    rgb(rgbmask & Bmask) = 1;
+
+    mask    = wavelength >= 440 & wavelength < 490;
+    rgbmask = get_rgb_mask (mask);
+    ## skiping red channel (values of zero)
+    rgb(rgbmask & Gmask) = (wavelength(mask) - 440) / 50; # 50 comes from 490-440
+    rgb(rgbmask & Bmask) = 1;
+
+    mask    = wavelength >= 490 & wavelength < 510;
+    rgbmask = get_rgb_mask (mask);
+    ## skiping red channel (values of zero)
+    rgb(rgbmask & Gmask) = 1;
+    rgb(rgbmask & Bmask) = -(wavelength(mask) - 510) / 20; # 20 comes from 510-490
+
+    mask    = wavelength >= 510 & wavelength < 580;
+    rgbmask = get_rgb_mask (mask);
+    rgb(rgbmask & Rmask) = (wavelength(mask) - 510) / 70; # 70 comes from 580-510
+    rgb(rgbmask & Gmask) = 1;
+    ## skiping blue channel (values of zero)
+
+    mask    = wavelength >= 580 & wavelength < 645;
+    rgbmask = get_rgb_mask (mask);
+    rgb(rgbmask & Rmask) = 1;
+    rgb(rgbmask & Gmask) = -(wavelength(mask) - 645) / 65; # 65 comes from 645-580
+    ## skiping blue channel (values of zero)
+
+    mask    = wavelength >= 645 & wavelength <= 780;
+    rgbmask = get_rgb_mask (mask);
+    rgb(rgbmask & Rmask) = 1;
+    ## skiping green channel (values of zero)
+    ## skiping blue channel (values of zero)
+
+    ## all other wavelengths have values of zero in all channels (black)
+  unwind_protect_cleanup
+    ## restore broadcats warning status
+    warning (bc_warn.state, "Octave:broadcast");
+  end_unwind_protect
+
+  ## let intensity fall off near the vision limits
+  ## set the factor
+  factor        = zeros (size (wavelength));
+  mask          = wavelength >= 380 & wavelength < 420;
+  factor(mask)  = 0.3 + 0.7*(wavelength(mask) - 380) / 40; # 40 = 420 - 380
+  mask          = wavelength >= 420 & wavelength <= 700;
+  factor(mask)  = 1;
+  mask          = wavelength > 700 & wavelength <= 780;
+  factor(mask)  = 0.3 + 0.7*(780 - wavelength(mask)) / 80; # 80 = 780 - 700
+  ## for other wavelengths, factor is 0
+
+  ## expand factor for the 3 channels
+  factor = repmat (factor, [ones(1, ndims (factor) - size_adjust) 3]);
 
   ## correct rgb
-  rgb = intensity_max .* (rgb .* factor) .^gamma .* ( rgb > 0 );
+  rgb = (rgb .* factor) .^gamma;
 
+  ## scale to requested class
+  switch tolower (out_class)
+    case {"single"}   rgb = im2single (rgb);
+    case {"double"}   ## do nothing, already class double
+    case {"uint8"}    rgb = im2uint8  (rgb);
+    case {"uint16"}   rgb = im2uint16 (rgb);
+    case {"int16"}    rgb = im2int16  (rgb);
+    otherwise         error ("wavelength2rgb: unsupported class `%s'", out_class)
+  endswitch
 endfunction
+
+%!demo
+%!
+%! RGB = wavelength2rgb (350:800); # RGB values for wavelengths between 350 and 800 nm
+%! RGB = RGB .* ones (300, 1, 1);  # make it 300 rows for display
+%! imshow (RGB)