view inst/montage.m @ 821:72cd72a3bff6

bestblk: complete rewrite to suppport multi-dimensional matrices. * bestblk.m: implement support for any number of dimensions. Vectorize code (loop only over dimensions now). Add tests. * NEWS: new section for functions now supporting N-dimensional matrices.
author Carnë Draug <carandraug@octave.org>
date Fri, 01 Nov 2013 05:05:28 +0000
parents 00409c568443
children 6ece20a81d0f
line wrap: on
line source

## Copyright (C) 2013 Carnë Draug <carandraug+dev@gmail.com>
##
## This program is free software; you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation; either version 3 of the License, or
## (at your option) any later version.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program; if not, see <http://www.gnu.org/licenses/>.

## -*- texinfo -*-
## @deftypefn  {Function File} {} montage (@var{I})
## @deftypefnx {Function File} {} montage (@var{X}, @var{cmap})
## @deftypefnx {Function File} {} montage (@var{filenames})
## @deftypefnx {Function File} {} montage (@dots{}, @var{param1}, @var{value1}, @dots{})
## @deftypefnx {Function File} {@var{h} =} montage (@dots{})
## Create montage from multiple images.
##
## The created montage will be of a single large image built from the 4D matrix
## @var{I}.  @var{I} must be a MxNx1x@var{P} or MxNx3x@var{P} matrix for a
## grayscale and binary, or RGB image with @var{P} frames.
##
## Alternatively, @var{X} can be a MxNx1x@var{P} indexed image with @var{P}
## frames, with the colormap @var{cmap}, or a cell array of @var{filenames}
## for multiple images.
##
## @table @asis
## @item DisplayRange
## A vector with 2 or 0 elements setting the highest and lowest value for
## display range.  It is interpreted like the @var{limits} argument to
## @code{imshow}.
##
## @item Indices
## A vector with the image indices to be displayed.  Defaults to all images,
## i.e., @code{1:size (@var{I}, 4)}.
##
## @item Size
## Sets the montage layout size.  Must be a 2 element vector setting
## [@var{nRows} @var{nCols}].  A value of NaN will be adjusted to the required
## value to display all images.  If both values are NaN (default), it will
## find the most square layout capable of displaying all of the images.
##
## @item MarginColor
## Sets color for the margins between panels.  Defaults to white.  Must be a
## 1 or 3 element vector for grayscale or RGB images.
##
## @item MarginWidth
## Sets width for the margins between panels.  Defaults to 0 pixels.  Note that
## the margins are only between panels.
##
## @item BackgroundColor
## Sets the montage background color. Defaults to black.  Must be a
## 1 or 3 element vector for grayscale or RGB images.  This will only affect
## montages with more panels than images.
## @end table
##
## The optional return value H is a graphics handle to the created plot.
##
## @seealso{imshow, padarray, permute, reshape}
## @end deftypefn

function h = montage (images, varargin)

  if (nargin < 1)
    print_usage ();
  endif
  warning ("off", "Octave:broadcast", "local");

  if (iscellstr (images))
    ## we are using cellfun instead of passing the "all" option
    ## to file_in_loadpath, so we know which of the figures was
    ## not found, and provide a more meaningful error message
    fullpaths = cellfun (@file_in_loadpath, images(:), "UniformOutput", false);
    lost      = cellfun (@isempty, fullpaths);
    if (any (lost))
      badpaths = strjoin (images(:)(lost), "\n");
      error ("montage: unable to find files:\n%s", badpaths);
    endif
    ## supporting grayscale, indexed, and truecolor images in
    ## the same list, complicates things a bit. Also, don't forget
    ## some images may be multipage
    infos = cellfun (@imfinfo, fullpaths, "UniformOutput", false);
    nImg  = sum (cellfun (@numel, infos));  # number of images

    ## in case of multipage images, different pages may have different sizes,
    ## so we must check the height and width of each page of each image
    height = infos{1}(1).Height;
    width  = infos{1}(1).Width;
    if (any (cellfun (@(x) any (arrayfun (@(y) y.Height != height || y.Width != width, x)), infos)))
      error ("montage: all images must have the same size.");
    endif

    ## To save on memory, we find the image with highest bitdepth, and
    ## create a matrix with the correct dimensions, where the images will
    ## be read into. We could read all the images and maps with a single
    ## cellfun call, and that would be a bit simpler than deducing
    ## from imfinfo but then we'd end up taking up the double of memory
    ## as we reorganize and convert the images

    ## a multipage image could have a mixture of colortypes, so we must
    ## check the type of every single one. If they are all grayscale, that's
    ## nice, it will be one dimension less, otherwise 3rd dimension is for
    ## rgb values. Also, any indexed image will be later converted to
    ## truecolor with rgb2ind so it will count as double
    if (any (cellfun (@(x) any (strcmp ({x(:).ColorType}, "indexed")), infos)))
      cl = "double";
      convt = @im2double;
    else
      maxbd = max (cellfun (@(x) max ([x(:).BitDepth]), infos));
      switch (maxbd)
        case {8}
          cl = "uint8";
          convt = @im2uint8;
        case {16}
          ## includes both uint18 and int16
          cl = "uint16";
          convt = @im2uint16;
        otherwise
          ## includes maxbd == 32 plus anything else. In case of anything
          ## unexpected, better play safe and use the one with more precision
          cl = "double";
          convt = @im2double;
      endswitch
    endif

    if (all (cellfun (@(x) all (strcmp ({x(:).ColorType}, "grayscale")), infos)))
      images = zeros (height, width, 1, nImg, cl);
    else
      images = zeros (height, width, 3, nImg, cl);
    endif

    nRead = 0; # count of pages already read
    for idx = 1:numel (infos)
      img_info   = infos{idx};
      nPages     = numel (img_info);
      nRead     += nPages;
      page_range = nRead+1-nPages:nRead;

      ## we won't be handling the alpha channel, but matlab doesn't either
      if (size (images, 3) == 1 || all (strcmp ({img_info(:).ColorType}, "truecolor")))
        ## sweet, no problems for sure
        [images(:,:,:,page_range), map] = imread (img_info(idx).Filename, 1:nPages);
      else
        [tmp_img, map] = imread (fullpaths(:), 1:nPages);
        if (! isempty (map))
          ## an indexed image. According to TIFF 6 specs, an indexed
          ## multipage image can have different colormaps for each page.
          ## How does imread behave with such images? And do they actually
          ## exist out there?
          tmp_img = ind2rgb (ind, map)
        elseif (size (tmp_img, 3) == 1)
          ## must be a grayscale image, propagate values to all channels
          tmp_img = repmat (tmp_img, [1 1 3 nPages])
        else
          ## must be a truecolor image, do nothing
        endif
        images(:,:,:,page_range) = tmp_img;
      endif
    endfor

  ## we can't really distinguish between a multipage indexed and normal
  ## image. So we'll assume it's an indexed if there's a second argument
  ## that is a colormap
  elseif (isimage (images) && nargin > 1 && iscolormap (varargin{1}))
    images = ind2rgb (images, varargin{1});
    varargin(1) = [];
  elseif (isimage (images))
    ## all is nice
  else
    print_usage ();
  endif

  [height, width, channels, nImg] = size (images);

  p = inputParser ();
  p.FunctionName = "montage";
  p = p.addParamValue ("DisplayRange",    [], @(x) isnumeric (x) && any (numel (x) == [0 2]));
  p = p.addParamValue ("Indices",         1:nImg, @(x) isindex (x, nImg));
  p = p.addParamValue ("Size",            [NaN NaN], @(x) isnumeric (x) && numel (x) == 2);
  p = p.addParamValue ("MarginWidth",     0, @(x) isnumeric (x) && isscalar (x));
  p = p.addParamValue ("MarginColor",     getrangefromclass (images)(2), @(x) isnumeric (x) && any (numel (x) == [1 3]));
  p = p.addParamValue ("BackgroundColor", getrangefromclass (images)(1), @(x) isnumeric (x) && any (numel (x) == [1 3]));
  p = p.parse (varargin{:});

  ## remove unecessary images
  images = images(:,:,:,p.Results.Indices);
  nImg   = size (images, 4);

  ## 1) calculate layout of the montage
  [nRows, nCols] = deal (p.Results.Size(1), p.Results.Size(2));
  if (isnan (nRows) && isnan (nCols))
    ## We must find the smallest layout that is most square. The most square
    ## are the ones with smallest difference between height and width. And to
    ## find the smallest layout for each number of columns, is the one that
    ## requires less rows. The smallest layout will be used as a mask to choose
    ## the minimum from hxW_diff
    v_heights = cumsum (linspace (height, nImg*height, nImg));
    v_widths  = cumsum (linspace (width,  nImg*width,  nImg));

    HxW_diff     = abs (v_heights' - v_widths);
    small_layout = ((1:nImg)' .* (1:nImg)) >= nImg;
    small_layout = logical (diff (padarray (small_layout, 1, 0, "pre")));
    HxW_diff(! small_layout) = Inf;
    [nRows, nCols] = find (HxW_diff == min (HxW_diff(:)), 1);

  elseif (isnan (nRows))
    nRows = ceil (nImg/nCols);
  elseif (isnan (nCols))
    nCols = ceil (nImg/nRows);
  elseif (nCols * nRows < nImg)
    error ("montage: size of %ix%i is not enough for image with %i frames.",
           nRows, nCols, nImg);
  endif

  ## 2) build the image
  margin_width = p.Results.MarginWidth;
  back_color   = fix_color (p.Results.BackgroundColor, channels);
  disp_img = zeros (height*nRows + margin_width*(nRows-1),
                    width *nCols + margin_width*(nCols-1),
                    channels, class (images)) + back_color;

  ## find the start and end coordinates for each of the images
  xRows = start_end (nRows, height, margin_width);
  xCols = start_end (nCols, width,  margin_width);

  ## Using reshape and permute to build the final image turned out to
  ## be quite a problem. So yeah... we'll use a for loop. Anyway, the number
  ## of images on a montage is never very high, this function is unlikely
  ## to be the speed bottleneck for any usage, and will make the code
  ## much more readable.
  iRow = iCol = 1;
  for iImg = 1:nImg
    if (iCol > nCols)
      iCol = 1;
      iRow++;
    endif
    rRow = xRows(1,iRow):xRows(2,iRow); # range of rows
    rCol = xCols(1,iCol):xCols(2,iCol); # range of columns
    disp_img(rRow,rCol,:) = images(:,:,:,iImg);
    iCol++;
  endfor

  ## 3) color margins as required
  margin_color = fix_color (p.Results.MarginColor, channels);
  if (margin_width > 0 && any (margin_color != back_color))
    mRows = linspace (xRows(2,1:end-1) +1, xRows(1,2:end) -1, margin_width) (:);
    mCols = linspace (xCols(2,1:end-1) +1, xCols(1,2:end) -1, margin_width) (:);

    ## a function that can be used to brodcast assignment
    bd_ass = @(x, y) subsasgn (x, struct ("type", "()", "subs", {{":"}}), y);
    disp_img(mRows,:,:) = bsxfun (bd_ass, disp_img(mRows,:,:), margin_color);
    disp_img(:,mCols,:) = bsxfun (bd_ass, disp_img(:,mCols,:), margin_color);
  endif

  ## 4) display the image
  ## because there is no default value for limits in imshow, we call imshow
  ## in this condition. Actually, the default is dependent on the image type
  ## which would make this even longer
  if (any (strcmpi (p.UsingDefaults, "DisplayRange")))
    tmp_h = imshow (disp_img, p.Results.DisplayRange);
  else
    tmp_h = imshow (disp_img);
  endif

  if (nargout > 0)
    h = tmp_h;
  endif
endfunction

## given number of elements (n), the length or each, and the border length
## between then, returns start and end coordinates for each of the elements
function [coords] = start_end (n, len, bord)
  coords      = bord * (0:(n-1)) + len * (0:(n-1)) + 1;
  coords(2,:) = coords(1,:) + len - 1;
endfunction

## color values can be given in grayscale or RGB values and may not match
## the values of the image.  This function will make the color match the
## image and give a 1x1x(1||3) vector that can be used for broadcasting
function color = fix_color (color, img_channels)
  if (numel (color) != img_channels)
    if (img_channels == 3)
      color = repmat (color, [3 1]);
    elseif (img_channels == 1)
      color = rgb2gray (reshape (color, [1 1 3]));
    else
      error ("montage: image has an unknown number (%d) of channels.", img_channels);
    endif
  endif
  color = reshape (color, [1 1 img_channels]);
endfunction