JMU
Sampled Static Visual Content
An Introduction with Examples in Java


Prof. David Bernstein
James Madison University

Computer Science Department
bernstdh@jmu.edu


Sampling Static Visual Content
Sampling Static Visual Content (cont.)
Examples of Sampled Static Visual Content
Important Java Classes
"Quick Start"
Reading Sampled Visual Content
javaexamples/visual/statik/sampled/ImageCanvasApp.java (Fragment: part2)
               Image                     image;
       ResourceFinder            finder;
       String                    name;
       
       // Get the file name
       name   = rootPaneContainer.getParameter("0");
       if (name == null) 
          name = "/visual/statik/sampled/scribble.gif";

       // Construct a ResourceFinder
       finder = ResourceFinder.createInstance();          

       // Read the image
       image  = null;       
       try
       {
          is = finder.findInputStream(name);
          if (is != null) 
          {
             image = ImageIO.read(is);
             is.close();             
          }
       }
       catch (IOException io)
       {
          // image will be null
       }
        
"Quick Start" (cont.)
Rendering Sampled Visual Content
javaexamples/visual/statik/sampled/ImageCanvas.java
        package visual.statik.sampled;


import java.awt.*;
import javax.swing.*;

/**
 * A concrete extension of a JComponent that illustrates
 * the rendering of sampled static visual content
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class ImageCanvas extends JComponent
{
    private Image        image;
    

    /**
     * Explicit Value Constructor
     *
     * @param image   The new Image
     */
    public ImageCanvas(Image image)
    {
       this.image = image;
    }

    /**
     * Render this ImageCanvas
     *
     * @param g   The rendering engine to use
     */
    public void paint(Graphics g)
    {
       Graphics2D         g2;
       
       // Cast the rendering engine appropriately
       g2 = (Graphics2D)g;
       
       // Render the image
       g2.drawImage(image,  // The Image to render
                    0,      // The horizontal coordinate
                    0,      // The vertical coordinate
                    null);  // An ImageObserver
    }

}

        
"Quick Start" (cont.)
The Complete App
javaexamples/visual/statik/sampled/ImageCanvasApp.java
        package visual.statik.sampled;


import java.awt.Image;
import java.io.*;
import javax.imageio.*;
import javax.swing.*;


import app.*;
import io.*;


/**
 * An example that illustrates the rendering of
 * sampled static visual content
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class   ImageCanvasApp
       extends AbstractMultimediaApp
{
    /**
     * Default Constructor
     *
     */
    public ImageCanvasApp()
    {
       super();    
    }


    /**
     * This method is called just before the main window
     * is first made visible
     */
    public void init()
    {
       Image                     image;
       ImageCanvas               canvas;
       InputStream               is;       
       JPanel                    contentPane;       
       ResourceFinder            finder;
       String                    name;
       
       // Get the file name
       name   = rootPaneContainer.getParameter("0");
       if (name == null) 
          name = "/visual/statik/sampled/scribble.gif";

       // Construct a ResourceFinder
       finder = ResourceFinder.createInstance();          

       // Read the image
       image  = null;       
       try
       {
          is = finder.findInputStream(name);
          if (is != null) 
          {
             image = ImageIO.read(is);
             is.close();             
          }
       }
       catch (IOException io)
       {
          // image will be null
       }

       // Create the component that will render the image
       canvas      = new ImageCanvas(image);
       canvas.setBounds(0,
                        0,
                        image.getWidth(null),
                        image.getHeight(null));

       // Add the ImageCanvas to the main window
       contentPane = (JPanel)rootPaneContainer.getContentPane();
       contentPane.add(canvas);
    }
}
        
Manipulating BufferedImage Objects
javaexamples/visual/statik/sampled/IdentityOp.java (Fragment: copy)
        
    /**
     * Copy a BufferedImage into a compatible BufferedImage
     *
     * @param src      The source image
     * @param dst An empty image in which to store the result
     */
    private void copy(BufferedImage src, BufferedImage dst)
    {
       ColorModel       dstColorModel, srcColorModel;
       int              dstRGB, height, srcRGB, width;
       int[]            dstColor, srcColor;       
       Raster           srcRaster;
       

       width            = src.getWidth();
       height           = src.getHeight();

       srcColorModel    = src.getColorModel();
       srcRaster        = src.getRaster();
       srcColor         = new int[4];
       
       dstColorModel = dst.getColorModel();
       
       for (int x=0; x<width; x++)
       {
          for (int y=0; y<height; y++)
          {
             srcRGB = src.getRGB(x, y);
             srcColorModel.getComponents(srcRGB,srcColor,0);

             dstRGB = dstColorModel.getDataElement(srcColor,0);
             dst.setRGB(x, y, dstRGB);                
          }
       }
    }
        
Constructing BufferedImage Objects
javaexamples/visual/statik/sampled/ImageFactory.java (Fragment: step)
            /**
     * Create a BufferedImage from an Image
     *
     * @param image       The original Image
     * @param channels    3 for RGB; 4 for ARGB
     * @return            The BufferedImage
     */
    public BufferedImage createBufferedImage(Image image,
                                             int   channels)
    {
       int                 type;
       if (channels == 3) type = BufferedImage.TYPE_INT_RGB;
       else               type = BufferedImage.TYPE_INT_ARGB;
       BufferedImage       bi;
       bi = null;
       bi = new BufferedImage(image.getWidth(null), 
                              image.getHeight(null),
                              type);
       Graphics2D          g2;
       g2 = bi.createGraphics();
       g2.drawImage(image, null, null);
       return bi;
    }
        
Constructing BufferedImage Objects (cont.)
From a File/Resource
javaexamples/visual/statik/sampled/ImageFactory.java (Fragment: method2)
            /**
     * Create a BufferedImage from a file/resource 
     * containing an Image
     *
     * @param name        The name of the file/resource
     * @param channels    3 for RGB; 4 for ARGB
     * @return            The BufferedImage
     */
    public BufferedImage createBufferedImage(String name, 
                                             int channels)
    {
       BufferedImage   image, result;       
       InputStream     is;       
       int             imageType;       
       

       image = null;       
       is    = finder.findInputStream(name);       

       if (is != null)
       {
          try
          {
             image       = ImageIO.read(is);       
             is.close();             
          }
          catch (IOException io)
          {
             image       = null;          
          }
       }

       // Modify the type, if necessary
       result = image;
       if (image != null)
       {
          imageType = image.getType();
          if (((channels == 3) && 
               (imageType != BufferedImage.TYPE_INT_RGB)) ||
              ((channels == 4) && 
               (imageType != BufferedImage.TYPE_INT_ARGB))    )
          {
             result = createBufferedImage(image, channels);
          }
       }
       
       return result;
    }
        
Operating on Sampled Static Visual Content
Operating on Sampled Content (cont.)
An IdentityOp
javaexamples/visual/statik/sampled/IdentityOp.java (Fragment: 1)
        
    /**
     * Creates a zeroed destination image with the correct size 
     * and number of bands (required by BufferedImageOp)
     *
     * @param src         The source image
     * @param dstCM  The ColorModel to be used in the created image
     */
    public BufferedImage createCompatibleDestImage(
                                             BufferedImage src,
                                             ColorModel    dstCM)
    {
       BufferedImage      dst;       
       int                height, width;
       WritableRaster     raster;
       

       if (dstCM == null) dstCM = src.getColorModel();
       
       height = src.getHeight();
       width  = src.getWidth();
       raster = dstCM.createCompatibleWritableRaster(width, 
                                                     height);
       

       dst = new BufferedImage(dstCM, raster,
                               dstCM.isAlphaPremultiplied(), 
                               null);
       
       return dst;       
    }
        
javaexamples/visual/statik/sampled/IdentityOp.java (Fragment: 2)
        
    /**
     * In general, performs a single-input/single-output operation on a 
     * BufferedImage (required by BufferedImageOp).
     *
     * If the destination image is null, a BufferedImage with an
     * appropriate ColorModel is created.
     *
     * In this case, this method simply "returns" a (copy of) the 
     * source.
     *
     * @param src      The source image
     * @param dst An empty image in which to srote the result (or null)
     */
    public BufferedImage filter(BufferedImage src, 
                                BufferedImage dst)
    {
       // Construct the destination image if one isn't provided
       if (dst == null)
       {
          dst = createCompatibleDestImage(src, 
                                          src.getColorModel());
       }

       // Copy the source to the destination
       copy(src, dst);

       // Return the destination (in case it is new)
       return dst;
    }
        
javaexamples/visual/statik/sampled/IdentityOp.java (Fragment: 3)
        
    /**
     * Returns the bounding box of the filtered destination image
     * (required by BufferedImageOp)
     *
     * @param src   The source image
     */
    public Rectangle2D getBounds2D(BufferedImage src)
    {
       Raster       raster;
       
       raster = src.getRaster();
       
       return raster.getBounds();       
    }
    

    /**
     * Returns the location of the corresponding destination point 
     * given a point in the source image (required by BufferedImageOp).
     *
     * If dstPt is specified, it is used to hold the return value.
     *
     * @param srcPt      The point in the source image
     * @param dstPt The point in the destination image
     */
    public Point2D getPoint2D(Point2D srcPt, Point2D dstPt)
    {
       if (dstPt == null) dstPt = (Point2D)srcPt.clone();
       dstPt.setLocation(srcPt);       

       return dstPt;
    }


    /**
     * Return the rendering hints for this operation
     * (required by BufferedImageOp).
     *
     * In this case, this method always returns null.
     */
    public RenderingHints getRenderingHints()
    {
       return null;
    }
        
Operating on Sampled Content (cont.)
A More Interesting Example: GrayExceptOp
javaexamples/visual/statik/sampled/GrayExceptOp.java (Fragment: skeleton)
        import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

import math.*;


/**
 * A BufferedImageOp that returns a gray-scale version
 * with one color (in a particular area) left unchanged
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class GrayExceptOp extends IdentityOp
{
    private int[]             highlightColor;
    
    
}
        
Operating on Sampled Content (cont.)
GrayExceptOp (cont.)
Operating on Sampled Content (cont.)
GrayExceptOp.areSimilar()
Operating on Sampled Content (cont.)
Metrics
javaexamples/math/Metric.java
        package math;

/**
 * A Metric is a function that satisfies the following properties
 * for all a, b, c:
 *
 *     distance(a,b) >= 0
 *     distance(a,b) == 0 iff a == b
 *     distance(a,b) == distance(b,a)
 *     distance(a,b) <= distance(a,c) + distance(b,c) 
 *
 * (The last of these properties is called the triangle inequality.)
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public interface Metric
{
    /**
     * Calculate the distance between two n-dimensional points
     *
     * @param a   One n-dimensional point
     * @param b   Another n-dimensional point
     * @return    The distance
     */
    public abstract double distance(double[] a, double[] b);    
    
}
        
javaexamples/math/RectilinearMetric.java
        package math;

/**
 * The rectilinear metric (i.e., the sum of the absolute values of the
 * differences between the elements).  This is sometimes also
 * called the Manhattan metric (because it is the distance you have to walk
 * between two points in a city that is layed out on a grid).
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class      RectilinearMetric
       implements Metric
{
    /**
     * Calculate the distance between two n-dimensional points
     * (required by Metric)
     *
     * Note: For simplicity, this method does not confirm that the
     * two arrays are the same size. It uses the smaller size.
     *
     * @param a   One n-dimensional point
     * @param b   Another n-dimensional point
     * @return    The distance
     */
    public double distance(double[] a, double[] b)
    {
       double  result;       
       int     n;
       
       result = 0.0;       
       n      = Math.min(a.length, b.length);

       for (int i=0; i<n; i++)
       {
          result += Math.abs(a[i]-b[i]);          
       }

       return result;       
    }
    

}
        
Operating on Sampled Content (cont.)
GrayExceptOp.areSimilar() (cont.)
javaexamples/visual/statik/sampled/GrayExceptOp.java (Fragment: areSimilar)
            /**
     * Determines if two colors are similar
     * 
     * Note: This method only uses the red, green, and 
     * blue components.  It does not use the alpha component.
     *
     * @param a   The components of one color
     * @param b   The components of the other color
     */
    private boolean areSimilar(int[] a, int[] b)
    {
       boolean        result;
       double         distance;
       
       for (int i=0; i<3; i++)
       {
          x[i] = a[i];
          y[i] = b[i];          
       }
       
       result   = false;
       distance = metric.distance(x, y);

       if (distance <= TOLERANCE) result = true;          

       return result;
    }
        
Operating on Sampled Content (cont.)
GrayExceptOp (cont.)
Operating on Sampled Content (cont.)
GrayExceptOp (cont.)
javaexamples/visual/statik/sampled/GrayExceptOp.java (Fragment: filter)
            /**
     * Perform the filtering operation
     *
     * @param source      The source image
     * @param destination An empty image in which to srote the result (or null)
     */
    public BufferedImage filter(BufferedImage src, 
                                BufferedImage dest)
    {
       ColorModel       destColorModel, srcColorModel;
       int              grayRGB, highlightRGB;
       int              srcRGB, srcHeight, srcWidth;
       int[]            gray, srcColor;       
       Raster           srcRaster;
       


       srcWidth      = src.getWidth();
       srcHeight     = src.getHeight();

       srcColorModel = src.getColorModel();
       srcRaster     = src.getRaster();
       srcColor      = new int[4];
       
       gray          = new int[4];
       
       
       if (dest == null) 
          dest = createCompatibleDestImage(src, 
                                           srcColorModel);

       destColorModel = dest.getColorModel();
       highlightRGB   = destColorModel.getDataElement(
                                         highlightColor, 
                                         0);       
       
       
       for (int x=0; x<srcWidth; x++)
       {
          for (int y=0; y<srcHeight; y++)
          {
             srcRGB = src.getRGB(x, y);
             srcColorModel.getComponents(srcRGB, srcColor, 0);


             if (areSimilar(srcColor, highlightColor))
             {
                dest.setRGB(x, y, highlightRGB);                
             }
             else
             {
                gray[0]=(srcColor[0]+srcColor[1]+srcColor[2])/3;
                gray[1]=gray[0];             
                gray[2]=gray[0];
                grayRGB=destColorModel.getDataElement(gray,0);
                dest.setRGB(x, y, grayRGB);                
             }
          }
       }
       
       return dest;
    }
        
Convolutions
Convolutions (cont.)

The Kernel as a Grid/Matrix

images/source-destination-images.gif

images/kernel.gif

images/spatial_convolution.gif

Convolutions (cont.)

Applying the Convolution to One Pixel

\( d_{i,j} = \sum_{r=-1}^{1} \sum_{c=-1}^{1} s_{i+r, j+c} k_{r,c} \)

Convolutions (cont.)

Identity Kernel

0 0 0
0 1 0
0 0 0

Convolutions (cont.)

Blurring Kernel

1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9
A System for Constructing Convolutions

Requirements

  1. Create different BufferedImageOp objects
  2. Conserve memory used for kernels
A System for Constructing Convolutions (cont.)

An Inflexible Factory

images/PresizedImageOpFactory.gif
A System for Constructing Convolutions (cont.)

An Wasteful Factory

images/WastefulImageOpFactory.gif
A System for Constructing Convolutions (cont.)

A Good Design

images/BufferedImageOpFactory.gif
A System for Constructing Convolutions (cont.)

A Low-Level Design that Leads to a Repetitive Implementation

javaexamples/visual/statik/sampled/RepetitiveBufferedImageOpFactory.java
        package visual.statik.sampled;

import java.awt.color.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;


/**
 * A class that can be used to construct BufferedImageOp objects that
 * can then be used to operate on static sampled visual content
 *
 * This (partial) implementation is difficult to maintain because it
 * contains a lot of repetitive code
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class RepetitiveBufferedImageOpFactory
{
    private Hashtable<Integer,ConvolveOp>  blurOps, edgeOps;


    private static RepetitiveBufferedImageOpFactory  instance = 
                    new RepetitiveBufferedImageOpFactory();
    

    /**
     * Default Constructor
     */
    private RepetitiveBufferedImageOpFactory()
    {
       blurOps   = new Hashtable<Integer,ConvolveOp>();
       edgeOps   = new Hashtable<Integer,ConvolveOp>();
    }


    /**
     * Create a blur operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createBlurOp(int size)
    {
       ConvolveOp    op;
       float         denom;      
       float[]       kernelValues;
       Integer       key;
       
       key = new Integer(size);
       op  = blurOps.get(key);
       if (op == null)
       {
          kernelValues = getBlurValues(size);

          op = new ConvolveOp(new Kernel(size,size,kernelValues),
                              ConvolveOp.EDGE_NO_OP,
                              null);

          blurOps.put(key, op);          
       }
       
       return op;
    }



    /**
     * Create an edge detection operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createEdgeDetectionOp(int size)
    {
       ConvolveOp    op;
       float         denom;      
       float[]       kernelValues;
       int           center;       
       Integer       key;
       
       key = new Integer(size);
       op  = edgeOps.get(key);
       if (op == null)
       {
          kernelValues = getEdgeValues(size);

          op = new ConvolveOp(new Kernel(size,size,kernelValues),
                              ConvolveOp.EDGE_NO_OP,
                              null);

          edgeOps.put(key, op);          
       }
       
       return op;
    }


    /**
     * Create a RepetitiveBufferedImageOpFactory object
     */
    public static RepetitiveBufferedImageOpFactory createFactory()
    {
       return instance;       
    }


   /**
    * Get the kernel values for a blurring convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private float[] getBlurValues(int size)
   {
      float       denom;      
      float[]     result;
      
      denom  = (float)(size*size);      
      result = new float[size*size];

      for (int row=0; row<size; row++)
         for (int col=0; col<size; col++)
            result[indexFor(row,col,size)] = 1.0f/denom;      

      return result;      
   }


   /**
    * Get the kernel values for an edge detecting convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private float[] getEdgeValues(int size)
   {
      float[]     result;
      int         center;
      
      
      center = size/2;
      result = new float[size*size];

      result[indexFor(center-1, center  , size)] = -1.0f;      
      result[indexFor(center  , center-1, size)] = -1.0f;      
      result[indexFor(center  , center  , size)] =  4.0f;      
      result[indexFor(center  , center+1, size)] = -1.0f;      
      result[indexFor(center+1, center  , size)] = -1.0f;      

      return result;      
   }
   

   /**
    * Convert row and column indexes (i.e., matrix indices)
    * into a linear index (i.e., vector index)
    *
    * @param row   The row index
    * @param col   The column index
    * @param size  The size of the square matrix
    */
   private int indexFor(int row, int col, int size)
   {
      return row*size + col;      
   }

}
        
A System for Constructing Convolutions (cont.)

A Good Low-Level Design

javaexamples/visual/statik/sampled/BufferedImageOpFactory.java (Fragment: createOp)
        
    /**
     * Create a ConvolveOp
     *
     * @param type   The type of ConvolveOp
     * @param size   The size of the kernel
     */
    private ConvolveOp createOp(Convolutions type, int size)
    {
       ConvolveOp                       op;
       Hashtable<Integer, ConvolveOp>   pool;
       Integer                          key;
       
       key  = new Integer(size);       
       pool = convolutionPools.get(type);
       op   = pool.get(key);
       
       if (op == null)
       {
          op = new ConvolveOp(new Kernel(size,size,
                                         type.getKernelValues(size)),
                                         ConvolveOp.EDGE_NO_OP,
                                         null);
          pool.put(key, op);          
       }

       return op;       
    }
        
A System for Constructing Convolutions (cont.)

An Unmaintainable Imeplementation of the Enumeration

javaexamples/visual/statik/sampled/UnmaintainableConvolutions.java
        package visual.statik.sampled;

/**
 * An enumeration of the different convolutions
 * that are supported in the BufferedImageOpFactory
 *
 * This (partial) implementation is difficult to maintain 
 * because each time a value is added the switch
 * statement must be changed.
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
enum UnmaintainableConvolutions
{
   BLUR,
   EDGE;
   
   

   /**
    * Get the kernel values
    *
    * @param size  The size of the convolution kernel
    */
   float[] getKernelValues(int size)
   {
      switch (this)
      {
         case BLUR:     return getBlurValues(size);
         case EDGE:     return getEdgeValues(size);
       
         default:       return getIdentityValues(size);
      }
   }
   
   
   /**
    * Get the kernel values for a blurring convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getBlurValues(int size)
   {
      float       denom;      
      float[]     result;
      
      denom  = (float)(size*size);      
      result = new float[size*size];

      for (int row=0; row<size; row++)
         for (int col=0; col<size; col++)
            result[indexFor(row,col,size)] = 1.0f/denom;      

      return result;      
   }


   /**
    * Get the kernel values for an edge detecting convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getEdgeValues(int size)
   {
      float[]     result;
      int         center;
      
      
      center = size/2;
      result = new float[size*size];

      result[indexFor(center-1, center  , size)] = -1.0f;      
      result[indexFor(center  , center-1, size)] = -1.0f;      
      result[indexFor(center  , center  , size)] =  4.0f;      
      result[indexFor(center  , center+1, size)] = -1.0f;      
      result[indexFor(center+1, center  , size)] = -1.0f;      

      return result;      
   }
   

   /**
    * Get the kernel values for an identity convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getIdentityValues(int size)
   {
      float[]     result;
      int         center;
      
      center = size/2;
      result = new float[size*size];
      result[indexFor(center,center,size)] = 1.0f;
      
      return result;      
   }
   

   /**
    * Convert row and column indexes (i.e., matrix indices)
    * into a linear index (i.e., vector index)
    *
    * @param row   The row index
    * @param col   The column index
    * @param size  The size of the square matrix
    */
   private static int indexFor(int row, int col, int size)
   {
      return row*size + col;      
   }

}
        
A System for Constructing Convolutions (cont.)

A Good Imeplementation of the Enumeration

javaexamples/visual/statik/sampled/Convolutions.java
        package visual.statik.sampled;

/**
 * An enumeration of the different convolutions
 * that are supported in the BufferedImageOpFactory
 *
 * Note that this class has package visibility because it
 * should only be used by classes in this package.
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
enum Convolutions
{
   BLUR {float[] getKernelValues(int size)
         {
            return getBlurValues(size);            
         } 
        },
   EDGE {float[] getKernelValues(int size)
         {
            return getEdgeValues(size);            
         }
        },
   EMBOSS {float[] getKernelValues(int size)
         {
            return getEmbossValues(size);            
         }
        },
   IDENTITY {float[] getKernelValues(int size)
         {
            return getIdentityValues(size);            
         }
        },
   SHARPEN {float[] getKernelValues(int size)
         {
            return getSharpenValues(size);            
         }
        };
   

   
   /**
    * Note that this method has package visibility
    */
   abstract float[] getKernelValues(int size);
   
   
   /**
    * Get the kernel values for a blurring convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getBlurValues(int size)
   {
      float       denom;      
      float[]     result;
      
      denom  = (float)(size*size);      
      result = new float[size*size];

      for (int row=0; row<size; row++)
         for (int col=0; col<size; col++)
            result[indexFor(row,col,size)] = 1.0f/denom;      

      return result;      
   }


   /**
    * Get the kernel values for an edge detecting convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getEdgeValues(int size)
   {
      float[]     result;
      int         center;
      
      
      center = size/2;
      result = new float[size*size];

      result[indexFor(center-1, center  , size)] = -1.0f;      
      result[indexFor(center  , center-1, size)] = -1.0f;      
      result[indexFor(center  , center  , size)] =  4.0f;      
      result[indexFor(center  , center+1, size)] = -1.0f;      
      result[indexFor(center+1, center  , size)] = -1.0f;      

      return result;      
   }


   /**
    * Get the kernel values for an embossing convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getEmbossValues(int size)
   {
      float[]     result;
      int         center;
      
      
      center = size/2;
      result = new float[size*size];

      result[indexFor(center-1, center-1, size)] = -2.0f;      
      result[indexFor(center  , center  , size)] =  1.0f;      
      result[indexFor(center+1, center+1, size)] =  2.0f;      

      return result;      
   }
   
   

   /**
    * Get the kernel values for an identity convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getIdentityValues(int size)
   {
      float[]     result;
      int         center;
      
      center = size/2;
      result = new float[size*size];
      result[indexFor(center,center,size)] = 1.0f;
      
      return result;      
   }


   /**
    * Get the kernel values for an sharpening convolution
    *
    * @param size   The size of the kernel
    * @return       The array of kernel values
    */
   private static float[] getSharpenValues(int size)
   {
      float[]     result;
      int         center;
      
      
      center = size/2;
      result = getEdgeValues(size);
      result[indexFor(center  , center  , size)] +=  1.0f;      

      return result;      
   }
   

   /**
    * Convert row and column indexes (i.e., matrix indices)
    * into a linear index (i.e., vector index)
    *
    * @param row   The row index
    * @param col   The column index
    * @param size  The size of the square matrix
    */
   private static int indexFor(int row, int col, int size)
   {
      return row*size + col;      
   }
   
}
        
A System for Constructing Convolutions (cont.)

A Good Factory

javaexamples/visual/statik/sampled/BufferedImageOpFactory.java
        package visual.statik.sampled;

import java.awt.*;
import java.awt.color.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.*;


/**
 * A class that can be used to construct BufferedImageOp objects that
 * can then be used to operate on static sampled visual content
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class BufferedImageOpFactory
{
    private ColorConvertOp   grayOp;
    

    private Hashtable<Convolutions, 
                      Hashtable<Integer, ConvolveOp>> convolutionPools;
    private Hashtable<Integer, GrayExceptOp>  grayExceptPool;    

    private LookupOp   negativeOp, nightVisionOp;
    private RescaleOp  brightenOp, darkenOp, metalOp;
    

    private static BufferedImageOpFactory     instance = 
                               new BufferedImageOpFactory();
    



    

    /**
     * Default Constructor
     */
    private BufferedImageOpFactory()
    {
       Hashtable<Integer, ConvolveOp>    pool;
       
       // Initialize the pool of ConvolveOp objects
       convolutionPools = new Hashtable<Convolutions, 
                                        Hashtable<Integer, ConvolveOp>>();
       
       for (Convolutions type : Convolutions.values())
       {
          pool = new Hashtable<Integer, ConvolveOp>();
          convolutionPools.put(type, pool);          
       }
       
       // Initialize the pool of grayExceptOp objects
       grayExceptPool = new Hashtable<Integer, GrayExceptOp>();
    }
    


    /**
     * Create a BufferedImageOpFactory object
     */
    public static BufferedImageOpFactory createFactory()
    {
       return instance;       
    }

    /**
     * Create a ConvolveOp
     *
     * @param type   The type of ConvolveOp
     * @param size   The size of the kernel
     */
    private ConvolveOp createOp(Convolutions type, int size)
    {
       ConvolveOp                       op;
       Hashtable<Integer, ConvolveOp>   pool;
       Integer                          key;
       
       key  = new Integer(size);       
       pool = convolutionPools.get(type);
       op   = pool.get(key);
       
       if (op == null)
       {
          op = new ConvolveOp(new Kernel(size,size,
                                         type.getKernelValues(size)),
                                         ConvolveOp.EDGE_NO_OP,
                                         null);
          pool.put(key, op);          
       }

       return op;       
    }


    /**
     * Create a blur operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createBlurOp(int size)
    {
       return createOp(Convolutions.BLUR, size);       
    }


    /**
     * Create a brighten operation
     */
    public RescaleOp createBrightenOp()
    {
       if (brightenOp == null)
       {
          brightenOp = new RescaleOp(1.5f, 0.0f, null);
       }
       return brightenOp;
    }


    /**
     * Create a darken operation
     */
    public RescaleOp createDarkenOp()
    {
       if (darkenOp == null)
       {
          darkenOp = new RescaleOp(0.5f, 0.0f, null);
       }
       return darkenOp;
    }

    /**
     * Create an edge detection operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createEdgeDetectionOp(int size)
    {
        return createOp(Convolutions.EDGE, size);
    }


    /**
     * Create an embossing operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createEmbossOp(int size)
    {
        return createOp(Convolutions.EMBOSS, size);
    }

    /**
     * Create an operation that converts to a gray colorspace
     */
    public ColorConvertOp createGrayOp()
    {
       if (grayOp == null)
       {
          ColorConvertOp         op;
          ColorSpace             cs;
          
          cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);          
          op = new ColorConvertOp(cs, null);
          grayOp = op;
       }
       return grayOp;       
    }


    /**
     * Create an operation that converts "all colors but one"
     * to gray
     *
     * Note: This method actually leaves all colors that are 
     * close to the specified color
     */
    public GrayExceptOp createGrayExceptOp(int r, int g, int b)
    {
       Color                color;
       GrayExceptOp         op;
       Integer              key;
       

       color = new Color(r,g,b);
       key   = new Integer(color.getRGB());
       
       op    = grayExceptPool.get(key);

       if (op == null)
       {
          
          op = new GrayExceptOp(r, g, b);
       }
       grayExceptPool.put(key, op);

       return op;       
    }



    /**
     * Create an operation that does not change the
     * image (i.e., an identity)
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createIdentityOp(int size)
    {
        return createOp(Convolutions.IDENTITY, size);
    }


    /**
     * Create a "metal" operation
     */
    public RescaleOp createMetalOp()
    {
       if (metalOp == null)
       {
          metalOp = new RescaleOp(1.0f, 128.0f, null);
       }
       return metalOp;
    }


    /**
     * Create a photo-negative operation
     */
    public LookupOp createNegativeOp()
    {
       if (negativeOp == null)
       {
          // Using a look-up operation to create a
          // "color negative"
          LookupTable               lookupTable;
          short[]                   lookup;
          
          lookup = new short[256];
          
          for (int i=0; i<lookup.length; i++) 
          {
             lookup[i] = (short)(255 - i);
          }       
          
          lookupTable = new ShortLookupTable(0, lookup);
          negativeOp  = new LookupOp(lookupTable, null);
       }
       
       return negativeOp;
    }


    /**
     * Create a night-vision operation (i.e., an operation that
     * makes eveything appear green)
     */
    public LookupOp createNightVisionOp()
    {
       if (nightVisionOp == null)
       {
          // Using a look-up operation to create a
          // "night vision" effect
          LookupTable               lookupTable;
          short[]                   leave, remove;
          short[][]                 lookupMatrix;
          
          
          leave  = new short[256];
          remove = new short[256];
          
          for (int i=0; i < leave.length; i++) {
             
             leave[i]  = (short)(i);
             remove[i] = (short)(0);
          }
          
          lookupMatrix    = new short[3][];
          
          lookupMatrix[0] = remove;
          lookupMatrix[1] = leave;
          lookupMatrix[2] = remove;
          
          
          lookupTable   = new ShortLookupTable(0, lookupMatrix);
          nightVisionOp = new LookupOp(lookupTable, null);
       }
        return nightVisionOp;
    }
    

    /**
     * Create a scaling operation
     *
     * @param xScale   The horizontal scaling factor
     * @param yScale   The vertical scaling factor
     */
    public AffineTransformOp createScaleOp(double xScale, double yScale)
    {
       // We could use an object pool in which 
       // the AffineTransform is the key

       AffineTransform      at;       
       AffineTransformOp    op;
       
       at = AffineTransform.getScaleInstance(xScale, yScale);
       op = new AffineTransformOp(
                          at, 
                          AffineTransformOp.TYPE_BILINEAR);

       return op;       
    }


    /**
     * Create a sharpen operation
     *
     * @param size   The size of the convolution kernel
     */
    public ConvolveOp createSharpenOp(int size)
    {
        return createOp(Convolutions.SHARPEN, size);
    }

}
        
Affine Transformations
Affine Transformations (cont.)

Scaling

javaexamples/visual/statik/sampled/BufferedImageOpFactory.java (Fragment: AffineTransformOp)
               AffineTransform      at;       
       AffineTransformOp    op;
       
       at = AffineTransform.getScaleInstance(xScale, yScale);
       op = new AffineTransformOp(
                          at, 
                          AffineTransformOp.TYPE_BILINEAR);
        
Affine Transformations (cont.)

Rotation

javaexamples/visual/statik/sampled/RotationApp.java (Fragment: rotate)
        
        AffineTransform   rotate;

        rotate = AffineTransform.getRotateInstance(
                                    theta, 
                                    before.getWidth() /2.0,
                                    before.getHeight()/2.0);

        RenderingHints           hints;

        // Value can be BILINEAR or NEAREST_NEIGHBOR
        hints = new RenderingHints(
                  RenderingHints.KEY_INTERPOLATION,
                  RenderingHints.VALUE_INTERPOLATION_BILINEAR
                                  );

        AffineTransformOp        op;

        op = new AffineTransformOp(rotate, hints);

        BufferedImage            after;

        after = op.filter(before, null);
        
Look-Ups
Look-Ups (cont.)

Creating a Photo Negative

javaexamples/visual/statik/sampled/BufferedImageOpFactory.java (Fragment: LookupOp1)
                  // Using a look-up operation to create a
          // "color negative"
          LookupTable               lookupTable;
          short[]                   lookup;
          
          lookup = new short[256];
          
          for (int i=0; i<lookup.length; i++) 
          {
             lookup[i] = (short)(255 - i);
          }       
          
          lookupTable = new ShortLookupTable(0, lookup);
          negativeOp  = new LookupOp(lookupTable, null);
        
Look-Ups (cont.)

"Night Vision"

javaexamples/visual/statik/sampled/BufferedImageOpFactory.java (Fragment: LookupOp2)
                  // Using a look-up operation to create a
          // "night vision" effect
          LookupTable               lookupTable;
          short[]                   leave, remove;
          short[][]                 lookupMatrix;
          
          
          leave  = new short[256];
          remove = new short[256];
          
          for (int i=0; i < leave.length; i++) {
             
             leave[i]  = (short)(i);
             remove[i] = (short)(0);
          }
          
          lookupMatrix    = new short[3][];
          
          lookupMatrix[0] = remove;
          lookupMatrix[1] = leave;
          lookupMatrix[2] = remove;
          
          
          lookupTable   = new ShortLookupTable(0, lookupMatrix);
          nightVisionOp = new LookupOp(lookupTable, null);
        
Color Space Conversion
javaexamples/visual/statik/sampled/BufferedImageOpFactory.java (Fragment: ColorConvertOp)
                  ColorConvertOp         op;
          ColorSpace             cs;
          
          cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);          
          op = new ColorConvertOp(cs, null);
        
Cropping/Cutting
Cropping/Cutting (cont.)

An Example

javaexamples/visual/statik/sampled/SurveillanceApp.java (Fragment: crop)
        
       cropped = before.getSubimage(x, y, width, height);

        
Design of a Sampled Static Visual Content System
Requirements
  1. Support the addition of sampled static visual content
  2. Support the removal of sampled static visual content
  3. Support \(z\)-ordering of sampled static visual content
  4. Support the transformation of sampled static visual content
  5. Support the rendering of sampled static visual content
Design of a Sampled Static Visual Content System (cont.)
Bad Alternatives (to add the functionality required for rendering)
Design of a Sampled Static Visual Content System (cont.)
A Good Design using Delegation
images/TransformableContent_sampled.gif
Implementing this Design
The Overall Structure
javaexamples/visual/statik/sampled/Content.java (Fragment: skeleton)
        package visual.statik.sampled;


import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;

/**
 * A BufferedImage that knows how to render itself 
 * (i.e., a BufferedImage along with all of 
 * the attributes necessary for rendering)
 *
 * @author  Prof. David Bernstein, James Madison Univeristy
 * @version 1.0
 */
public class Content
             extends    visual.statik.AbstractTransformableContent
             implements TransformableContent

{
    private boolean            refiltered;    
    private BufferedImageOp    imageOp;    
    private Composite          composite;    
    private BufferedImage      originalImage, transformedImage;    
    private Rectangle2D.Double originalBounds,transformedBounds;
    
    
    private static final double       DEFAULT_X         = 0.0;
    private static final double       DEFAULT_Y         = 0.0;
    

    

    /**
     * Default Constructor
     */
    public Content()
    {
       this(null, DEFAULT_X, DEFAULT_Y);
    }



    /**
     * Explicit Value Constructor
     *
     * @param image        The BufferedImage
     * @param x            The coordinate of the left edge
     * @param y            The coordinate of the top edge
     */
    public Content(BufferedImage  image,  
                                double x, double y)
    {
       this(image, x, y, true);       
    }
}
        
Implementing this Design (cont.)
The "Setters"
javaexamples/visual/statik/sampled/Content.java (Fragment: set)
            /**
     * Set the BufferedImageOp to use when transforming
     * the Image
     *
     * @param op   The BufferedImageOp
     */
    public void setBufferedImageOp(BufferedImageOp op)
    {
       imageOp    = op;       
       refiltered = true;
    }
    


    /**
     * Set the transparency/Composite
     *
     * @param c   The Composite
     */
    public void setComposite(Composite c)
    {
       composite = c;;
    }


    /**
     * Set the location
     *
     * Note: This method overrides the version in the parent because
     * the translation of sampled content does not require 
     * transformation
     *
     * @param x   The x position
     * @param y   The y position
     */
    public void setLocation(double x, double y)
    {
       if (!rotatable)
       {
          this.x = x;
          this.y = y;

          transformedBounds.x      = this.x;
          transformedBounds.y      = this.y;
       }
       else
       {
          super.setLocation(x, y);          
       }
    }



    /**
     * Set the rotation
     *
     * Note: This method overrides the version in the parent because
     * efficiency can be improved when rotations are
     * prevented
     *
     * @param angle  The rotation angle
     * @param x       The x-coordinate of the point to rotate around
     * @param y       The y-coordinate of the point to rotate around
     */
    public void setRotation(double angle, double x, double y)
    {
       if (rotatable) super.setRotation(angle, x, y);       
    }
        
Implementing this Design (cont.)
Transformations
javaexamples/visual/statik/sampled/Content.java (Fragment: transform)
            /**
     * Create the transformed version of the shape
     */
    private void createTransformedContent()
    {
       createTransformedContent(getAffineTransform());
    }


    /**
     * Create a transformed version of the BufferedImage
     *
     * @param at   The AffineTransform to use
     */
    private void createTransformedContent(AffineTransform at)
    {
       AffineTransformOp       op;
       
       op = new AffineTransformOp(at, AffineTransformOp.TYPE_BILINEAR);
       createTransformedContent(op);       
    }


    /**
     * Create a transformed version of the BufferedImage
     *
     * @param op   The BufferedImageOp to use
     */
    private void createTransformedContent(BufferedImageOp op)
    {
       BufferedImage   tempImage;       
       Rectangle2D     temp;

       try
       {
          // Apply the filter
          tempImage = originalImage;          
          if (imageOp != null) 
          {
             tempImage = imageOp.filter(originalImage, null);             
          }
          
          // Create the transformed version
          transformedImage = op.filter(tempImage, null);

          temp = op.getBounds2D(originalImage);
          
          transformedBounds.x      = temp.getX();
          transformedBounds.y      = temp.getY();
          transformedBounds.width  = temp.getWidth();
          transformedBounds.height = temp.getHeight();  

          if (!rotatable)
          {
             transformedBounds.x += x;
             transformedBounds.y += y;
          }

          setTransformationRequired(false);
       }
       catch (RasterFormatException rfe)
       {
          // Unable to transform
          transformedImage = null;          
       }
    }
        
Implementing this Design (cont.)
Rendering
javaexamples/visual/statik/sampled/Content.java (Fragment: render)
            /**
     * Render this sampled visual content
     * (required by Content)
     *
     * @param g   The rendering engine to use
     */
    public void render(Graphics g)
    {
       Composite      oldComposite;
       Graphics2D     g2;
       
       g2 = (Graphics2D)g;


       if (originalImage != null)
       {
          oldComposite = g2.getComposite();
          if (composite != null) g2.setComposite(composite);       
          

          // Transform the Image (if necessary)
          if (isTransformationRequired())
          {
             createTransformedContent();
          }

          // Render the image
          if (!rotatable)
             g2.drawImage(transformedImage,(int)x,(int)y,null);          
          else
             g2.drawImage(transformedImage,0,0,null);          


          g2.setComposite(oldComposite);       
       }       
    }
        
Implementing this Design (cont.)
A Factory
javaexamples/visual/statik/sampled/ContentFactory.java
        package visual.statik.sampled;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.Arrays;

import io.ResourceFinder;

/**
 * A utility class for constructing/creating 
 * visual.statik.sampled.Content objects
 *
 * @author  Prof. David Bernstein, James Madison University
 * @version 1.0
 */
public class ContentFactory
{
    private ImageFactory          imageFactory;
    
    private static final int      DEFAULT_CHANNELS = 3;
    private static final boolean  ROTATABLE        = true;    

    /**
     * Default Constructor
     */
    public ContentFactory()
    {
       super();       
       imageFactory = new ImageFactory();
    }
    

    /**
     * Create a Content from a BufferedImage
     *
     * @param image     The BufferedImage
     * @param rotatable false to prevent rotations (and improve performance)
     * @return          The Content
     */
    public Content createContent(BufferedImage image,
                                 boolean rotatable)
    {
       return new Content(image, 0, 0, rotatable);       
    }
    

    /**
     * Create a Content from a BufferedImage
     *
     * @param image     The BufferedImage
     * @return          The Content
     */
    public Content createContent(BufferedImage image)
    {
       return new Content(image, 0, 0, ROTATABLE);       
    }



    /**
     * Create a Content from an Image
     *
     * @param image      The original Image
     * @param channels   3 for RGB; 4 for ARGB
     * @param rotatable  false to prevent rotations (and improve performance)
     * @return           The Content
     */
    public Content createContent(Image   image,
                                 int     channels,
                                 boolean rotatable)
    {
       BufferedImage       bi;

       bi = imageFactory.createBufferedImage(image, channels);
       return createContent(bi, rotatable);
    }


    /**
     * Create a Content from an Image
     *
     * @param image      The original Image
     * @param channels   3 for RGB; 4 for ARGB
     * @return           The Content
     */
    public Content createContent(Image image,
                                 int   channels)
    {
       return createContent(image, channels, ROTATABLE);
    }


    /**
     * Create a Content (with the default number
     * of channels) from an Image
     *
     * @param image      The original Image
     * @param rotatable  false to prevent rotations (and improve performance)
     * @return           The Content
     */
    public Content createContent(Image image,
                                 boolean rotatable)
    {
       return createContent(image, DEFAULT_CHANNELS, 
                            rotatable);
    }



    /**
     * Create a Content (with the default number
     * of channels) from an Image
     *
     * @param image      The original Image
     * @return           The Content
     */
    public Content createContent(Image image)
    {
       return createContent(image, 
                            DEFAULT_CHANNELS, 
                            ROTATABLE);
    }



    /**
     * Create a Content from a file/resource
     * containing an Image
     *
     * @param name       The name of the file/resource
     * @param channels   3 for RGB; 4 for ARGB
     * @param rotatable  false to prevent rotations (and improve performance)
     * @return           The Content
     */
    public Content createContent(String name,
                                 int channels,
                                 boolean rotatable)
    {
       BufferedImage          bi;
       
       bi = imageFactory.createBufferedImage(name, channels);
       return createContent(bi, rotatable);
    }



    /**
     * Create a Content from a file/resource
     * containing an Image
     *
     * @param name       The name of the file
     * @param channels   3 for RGB; 4 for ARGB
     * @return           The Content
     */
    public Content createContent(String name,
                                 int channels)
    {
       return createContent(name,channels, ROTATABLE);
    }


    /**
     * Create a Content (with the default number of
     * channels) from a file containing an Image
     *
     * @param name       The name of the file
     * @param rotatable  false to prevent rotations (and improve performance)
     * @return           The Content
     */
    public Content createContent(String name, 
                                 boolean rotatable)
    {
       return createContent(name, 
                            DEFAULT_CHANNELS, 
                            rotatable);
    }


    /**
     * Create a Content (with the default number of
     * channels) from a file/resource containing an Image
     *
     * @param name       The name of the file
     * @return           The Content
     */
    public Content createContent(String name)
    {
       return createContent(name, 
                            DEFAULT_CHANNELS, 
                            ROTATABLE);
    }



    /**
     * Create an array of Content objects 
     * from an array of images in files/resources
     *
     * @param name     The names of the file/resource
     * @param channels 3 for RGB, 4 for ARGB
     * @return         The Content objects
     */
    public Content[] createContents(String[] names,
                                    int    channels)    
    {
       BufferedImage[]   images;
       Content[]         result;
       int               n;
       

       n      = names.length;       
       images = imageFactory.createBufferedImages(names, 
                                                  channels);
       result = new Content[n];        

       for (int i=0; i<n; i++)
       {
          result[i] = createContent(images[i], ROTATABLE);
       }
        
       return result;
    }    



    /**
     * Create an array of Content objects from a 
     * group of files/resources containing images
     *
     * @param path        The path to the directory containing the images
     * @param filter      The FilenameFilter to use
     * @param channels    3 for RGB; 4 for ARGB
     * @return            The array of Content objects
     */
    public Content[] createContents(String         path,
                                    FilenameFilter filter,
                                    int            channels)
    {
       File            dir;
       int             length;       
       Content[]       rbi;       
       String[]        fileNames;

       
       dir = new File(path);

       fileNames = dir.list(filter);
       Arrays.sort(fileNames);

       length = fileNames.length;
       rbi = new Content[length];

       for (int i=0; i < length; i++)
       {
          rbi[i] = createContent(fileNames[i], 
                                 channels, 
                                 ROTATABLE);
       }
       return rbi;       
    }


    /**
     * Create an array Content (with the default
     * number of channels) from a group of files containing images
     *
     * @param path    The path to the directory containing the images
     * @param filter  The FilenameFilter to use
     * @return        The array of Content objects
     */
    public Content[] createContents(String path,
                                    FilenameFilter filter)
    {
       return createContents(path, filter, DEFAULT_CHANNELS);
    }

    /**
     * Create an array of Content objects 
     * from an "array" of images in a file
     *
     * @param name     The name of the file/resource
     * @param n        The number of images
     * @param channels 3 for RGB, 4 for ARGB
     * @return         The Content objects or null if an Exception was thrown
     */
    public Content[] createContents(String name,
                                    int    n, 
                                    int    channels)    
    {
       BufferedImage[]   images;
       Content[]         result;
        

       images = imageFactory.createBufferedImages(name, n, 
                                                  channels);
        
       result = new Content[n];        
       for (int i=0; i<n; i++)
       {
          result[i] = createContent(images[i], ROTATABLE);
       }
        
       return result;
    }    



    /**
     * Create an array of Content objects 
     * from a table-oriented Image in a file
     *
     * @param name     The name of the file/resource
     * @param rows     The number of rows
     * @param columns  The number of columns
     * @param channels 3 for RGB, 4 for ARGB
     * @return         The Content objects or null if an Exception was thrown
     */
    public Content[][] createContents(String name, 
                                      int rows, 
                                      int columns, 
                                      int channels)
    {
       BufferedImage[][]   images;
       Content[][]         result;
        

       images = imageFactory.createBufferedImages(name, 
                                                  rows, 
                                                  columns, 
                                                  channels);
        
       result = new Content[rows][columns];        
       for (int r=0; r<rows; r++)
       {
          for (int c=0; c<columns; c++)
          {
             result[r][c] = createContent(images[r][c], 
                                          ROTATABLE);
          }
       }
        
       return result;
    }    

}