Module 2: Multidimensional Arrays


Supplemental material


A simple example - magic squares

A magic square of size N is an arrangement of the numbers 1,2, ..., N2 in a square array so that the row sums and column sums are all identical. For example, you can see that the rows and columns of this 3 x 3 square add up to 15:

6
1
8
7
5
3
2
9
4

About magic squares:

For odd squares, there is a simple algorithm called the Staircase Algorithm:

public class MagicSquare {

    public static void main (String[] argv)
    {
        // Some test cases.
        int[][] square = generateSquare (3);
        print (square);

        square = generateSquare (5);
        print (square);
    }
    

    // Current row/column and next row/column.
    static int row, col;
    static int nextRow, nextCol;


    static int[][] generateSquare (int size)
    {
        // This algorithm only works for odd-sizes.
        if (size % 2 != 1) {
            System.out.println ("size must be odd");
            System.exit (0);
        }
        
        int[][] A = new int [size][size];

        // Start with middle in top row.
        row = 0;
        col = size/2;
        A[row][col] = 1;
        for (int n=2; n<=size*size; n++) {

            // Go up diagonally to the right.
            computeNext (size);

            if (A[nextRow][nextCol] == 0) {
                // Place next number here if unoccupied.
                A[nextRow][nextCol] = n;
            }
            else {
                // Else, place directly below current number.
                nextRow = row + 1;
                nextCol = col;
                A[nextRow][nextCol] = n;
            }

            // Update.
            row = nextRow;
            col = nextCol;

        } //end-for

        return A;
    }


    static void computeNext (int size)
    {
        if (row == 0) {
            // If we're at the top, next row wraps around.
            nextRow = size - 1;
        }
        else {
            // Otherwise, go to previous row.
            nextRow = row - 1;
        }
        
        if (col == size-1) {
            // If we're at the rightmost col, wrap around to leftmost.
            nextCol = 0;
        }
        else {
            // Otherwise, next column is the one to the right.
            nextCol = col + 1;
        }
    }
    


    static void print (int[][] A)
    {
        System.out.println ("Square: ");
        for (int i=0; i < A.length; i++) {
            for (int j=0; j < A[i].length; j++) {
                System.out.printf ("%3d", A[i][j]);
            }
        }
    }
    
}
Note:

In-Class Exercise 1: Download the above program and execute. Then modify the print() method so that it prints out row and column sums as follows:

Square of size 3: 
  8  1  6 | 15
  3  5  7 | 15
  4  9  2 | 15
---------
 15 15 15

Square of size 5: 
 17 24  1  8 15 | 65
 23  5  7 14 16 | 65
  4  6 13 20 22 | 65
 10 12 19 21  3 | 65
 11 18 25  2  9 | 65
---------------
 65 65 65 65 65
  


2D arrays in memory

Let's look at a simpler example

public class MagicSquare2 {

    public static void main (String[] argv)
    {
        // Some test cases.
        int[][] square = {
            {8, 1, 6},
            {3, 5, 7},
            {4, 9, 2}
        };
        print (square);
    }
    
    static void print (int[][] A)
    {
       // ... same as in MagicSquare.java ...
    }
    
}
Note:

Now let's consider a variation

public class MagicSquare3 {

    public static void main (String[] argv)
    {
        // Create space for the array, but not assign individual values:
        int[][] square = new int [3][3];
        assignValues (square);
        print (square);
    }
    
    static void assignValues (int[][] A)
    {
        // ... the Staircase algorithm ...
    }

    static void print (int[][] A)
    {
        // ... same as before ...
    }

}
Note:

Because arrays are implemented via pointers, we can create irregular arrays.

public class Triangle {

    public static void main (String[] argv)
    {
        // Different-length rows.
        int[][] A = {
            {1},
            {2, 1},
            {3, 2, 1},
            {4, 3, 2, 1},
            {5, 4, 3, 2, 1}
        };
        print (A);
    }

    static void print (int[][] A)
    {
        // The number of rows is still A.length.
        for (int i=0; i < A.length; i++) {

            System.out.print ("Row i=" + i + " has " + A[i].length + " elements: ");

            // Note: we must use A[i].length, since they are all different.
            for (int j=0; j < A[i].length; j++) {
                System.out.print (" " + A[i][j]);
            }

            System.out.println ();
        }
    }
    
}
Note:


Images as 2D arrays

What is an image?

Image file formats:

  • While images in main memory are stored as 2D arrays of pixels, a file need not store all pixels.
        ⇒ instead, images are compressed (to take up less space)

  • There are three popular file formats:
    • GIF: a lossless compression format, but with some legal problems.
    • PNG: a lossless version of GIF without the legal problems.
    • JPEG: a lossy format (much better compression).

Images in Java:

  • Java uses the Image class to hold image data.

  • Java supports the reading/writing of images in all three formats: GIF, JPG and PNG.

  • The format is recognized from the file type (usually .gif, .jpg or .png).

  • To manipulate pixels, we need the array of pixels.

  • For some reason, Java has made it cumbersome to get the pixels. Instead, you can use our own ImageTool.java, which simplifies pixel-access.

Useful methods in ImageTool:

  • The very first thing to do is to make an instance of the tool:
            ImageTool imTool = new ImageTool ();
        

  • Use readImageFile() to read an image from a file:
          Image image = imTool.readImageFile ("mymugshot.jpg");
        

  • Use imageToPixels() to extract the pixels:
          int[][][] pixels = im.imageToPixels (image);
        

  • Use pixelsToImage() to convert pixels to an Image:
          Image image2 = imTool.pixelsToImage (pixels);
        

  • Use writeToJPGFile() to create a JPG file:
          imTool.writeToJPGFile (image, "doctoredMugshot.jpg");
        

  • Finally, use showImage() to view an image:
          imTool.showImage (image);
        

Notice that the pixels are a 3D array:

      int[][][] pixels = imTool.imageToPixels (image);
    
  • Here, pixels[i][j] is the pixel in the i-th row and j-th column.

  • Each pixel is represented by four integers:
    • pixels[i][j][0] is the alpha value (the transparency, which for the most part we won't use).
    • pixels[i][j][1] is the red intensity.
    • pixels[i][j][2] is the green intensity.
    • pixels[i][j][3] is the blue intensity.

  • Each pixel intensity is an integer between 0 and 255 (inclusive).

  • For example, here's how we can create a pure-white image with 500 x 500 pixels:
            int size = 500;
            pixels = new int [size][size][4];
            for (int i=0; i < pixels.length; i++) {
                for (int j=0; j < pixels[0].length; j++) {
                    for (int k=0; k < 4; k++) {
                        pixels[i][j][k] = 255;
                    }
                }
            }
      

  • Let's draw a black box inside this image:
            int start = size/4;
            int end = (int) (3.0*size/4.0);
            for (int i=start; i <= end; i++) {
                for (int j=start; j <= end; j++) {
                    for (int k=1; k < 4; k++) {
                        pixels[i][j][k] = 0;
                    }
                }
            }
      

  • To view the image:
            ImageTool imTool = new ImageTool ();
      	Image image = imTool.pixelsToImage (pixels);
    	imTool.showImage (image);
      


Image transformations

Now let's manipulate some image pixels, starting with a "Thumbnail" application:

  • We'll read in an image and convert to pixels.

  • Then, we'll create a new image that's smaller and display that (the thumbnail).
Here's the program:
import java.awt.*;
import java.awt.image.*;

public class ImageShrinker {

    // We'll declare this up here so that it can be shared
    // across methods.
    static ImageTool imTool = new ImageTool ();


    public static void main (String[] argv)
    {
        // Read in an image and display it.
        Image image = imTool.readImageFile ("bugs.jpg");
        imTool.showImage (image, "original");

        // All the work of shrinking is done in the shrink() method.
        // We'll shrink to 20% of the current size.
        Image shrunkImage = shrink (image, 0.2);

        // Now display the result.
        imTool.showImage (shrunkImage, "shrunk");
    }


    static Image shrink (Image image, double factor)
    {
        // Extract the pixels.
        int[][][] pixels = imTool.imageToPixels (image);

        // The array dimensions.
        int numRows = pixels.length;
        int numCols = pixels[0].length;

        // The resulting shrunk image will have fewer rows and columns.
        int numTargetRows = (int) (factor * numRows);
        int numTargetCols = (int) (factor * numCols);

        // What we'll do is skip pixels in the original. This is the 
        // skip amount:
        int multiplier = numRows / numTargetRows;

        // Make a pixel array to hold the shrunk image.
        int[][][] shrunkPixels = new int [numTargetRows][numTargetCols][4];

        // Now fill the pixel values in the new image.
        for (int i=0; i < numTargetRows; i++) {
            for (int j=0; j < numTargetCols; j++) {
                // For each pixel in the new image, we'll get
                // one of the original pixels as determined by the "skip".
                int originalRow = i * multiplier;
                int originalCol = j * multiplier;

                // Pay attention to this pointer copy:
                shrunkPixels[i][j] = pixels[originalRow][originalCol];
            }
        }

        // Now convert pixels into an instance of Image and return that.
        Image shrunkImage = imTool.pixelsToImage (shrunkPixels);
        return shrunkImage;
    }
    
}
Note:
  • Let's take a second look at this line:
                    shrunkPixels[i][j] = pixels[originalRow][originalCol];
        
    • First, recall that shrunkPixels is a 3D array (not 2D).
    • That means we should expect to use it as
                      shrunkPixels[i][j][k] = something ...
            
    • So, how come we use only two dimensions?

  • To see how it works, first let's consider an alternative to the above:
                    shrunkPixels[i][j][0] = pixels[originalRow][originalCol][0];
                    shrunkPixels[i][j][1] = pixels[originalRow][originalCol][1];
                    shrunkPixels[i][j][2] = pixels[originalRow][originalCol][2];
                    shrunkPixels[i][j][3] = pixels[originalRow][originalCol][3];
        
    • This is correct because we copy over each of the four pixel values.
    • Alternatively, we could also do:
                      for (int k=0; k < 4; k++) {
                          shrunkPixels[i][j][k] = pixels[originalRow][originalCol][k];
                      }
            

  • So, again, why did the 2D copy work?
    • The reason is, we performed a pointer copy
    • To see what that means, suppose that i=2, j=1 in the loop, and that originalRow=10, originalCol=5.

    • When the copy occurs, both shrunkPixels[2][1] and pixels[10][5] point to the same block of four pixels.

In general, pointer copies are efficient but require care. They should not be used if the original or copy will be modified, as this example shows:

public class PointerCopyProblem {

    public static void main (String[] argv)
    {
        // Make a 3 x 2 array.
        int[][] A = {
            {1, 2},
            {3, 4},
            {5, 6}
        };
        
        // Now make a new array B that is to be a copy of A.

        // To B, we assign space for 3 pointers:
        int[][] B = new int [3][];
        for (int i=0; i < A.length; i++) {
            B[i] = A[i];
        }

        print (A, B);
        
        // Make a change in B.
        B[0][0] = 9;
        
        print (A, B);
    }

    static void print (int[][] A, int[][] B)
    {
        System.out.println ("\nArray A: ");
        for (int i=0; i < A.length; i++) {
            for (int j=0; j < A[i].length; j++) {
                System.out.print (" " + A[i][j]);
            }
            System.out.println ();
        }
        
        System.out.println ("Array B: ");
        for (int i=0; i < B.length; i++) {
            for (int j=0; j < B[i].length; j++) {
                System.out.print (" " + B[i][j]);
            }
            System.out.println ();
        }
    }
    
}
The output is:
Array A: 
 1 2
 3 4
 5 6
Array B: 
 1 2
 3 4
 5 6

Array A: 
 9 2
 3 4
 5 6
Array B: 
 9 2
 3 4
 5 6
Thus, array A is modified.

In-Class Exercise 2: Download the above code and fix the problem. That is, do the copy correctly.


More image transformations

Let's continue image transformations with enlargement: take an image and make it larger:

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

public class ImageEnlarger {

    static ImageTool imTool = new ImageTool ();


    public static void main (String[] argv)
    {
        Image image = imTool.readImageFile ("bugsThumb.jpg");
        imTool.showImage (image, "original");

        Image enlargedImage = enlarge (image, 5);

        imTool.showImage (enlargedImage, "enlarged");
    }


    static Image enlarge (Image image, double factor)
    {
        // Extract the pixels and array dimensions.
        int[][][] pixels = imTool.imageToPixels (image);
        int numRows = pixels.length;
        int numCols = pixels[0].length;

        // The resulting shrunk image will have more rows and columns.
        int numTargetRows = (int) (factor * numRows);
        int numTargetCols = (int) (factor * numCols);

        int multiplier = numTargetRows / numRows;

        // Make the pixel array for the target.
        int[][][] enlargedPixels = new int [numTargetRows][numTargetCols][4];

        for (int i=0; i < numRows; i++) {
            for (int j=0; j < numCols; j++) {

                // Copy a single pixel from the smaller into a region of the larger.
                int startRow = i * multiplier;
                int startCol = j * multiplier;
                for (int m=0; m < multiplier; m++) {
                    for (int n=0; n < multiplier; n++) {
                        // This time, we'll do the copy correctly.
                        for (int k=0; k < 3; k++) {
                            enlargedPixels[startRow+m][startCol+n][k] = pixels[i][j][k];
                        }
                    }
                }
                // End-copy-region
            }
        }

        Image enlargedImage = imTool.pixelsToImage (enlargedPixels);
        return enlargedImage;
    }
    
}
Note:
  • The enlarged image has more pixels
         ⇒ this means we have to "make up" pixels in the enlarged image.

  • To solve the problem, we simply copy over a single pixel into a region (collection of pixels) of the enlarged image.
         ⇒ see the nested double-for-loop.

  • This time around, we did the copying correctly (i.e., NOT a pointer copy).

In-Class Exercise 3: Download and execute the above code. You'll also need a thumbnail. Either find one yourself or use this one. Why is the enlarged image so grainy?

In-Class Exercise 4: Write code to convert a color image to a greyscale image (with only shades of grey). To do this, recall that every pixel is four values (alpha, red, green, blue). If you make the values of red, green and blue the same, it becomes grey. So, what you need to do is:

  • Download this template and this test image.
  • Implement your code in the method toGreyScale(). The pixels for the original image and the creation of the pixel array for the grey-image have been done for you.
  • Remember to copy the alpha value without changing it.
  • To make the RGB values the same in the grey-image, simply take the average of the R, G and B values in the original.
The structure of the code will be similar to the other image examples we've seen. Can you identify the who's in the image and where it is?

Next, let's consider a linear map:

  • In this transformation, each pixel value is multiplied by a constant and added to a second constant.

  • Linear mappings are useful for contrast (the multiplication) and brightness (the addition).
Here's the program:
import java.awt.*;
import java.awt.image.*;

public class LinearMap {

    static ImageTool imTool = new ImageTool ();

    public static void main (String[] argv)
    {
        // Read in an image and display.
        Image image = imTool.readImageFile ("prof.jpg");
        imTool.showImage (image, "original");

        // Convert to grey scale and display.
        Image mappedImage = linearMap (image, 2.0, 1.0);
        imTool.showImage (mappedImage, "mapped image");
    }

    static Image linearMap (Image image, double a, double b)
    {
        // Extract pixels and size.
        int[][][] pixels = imTool.imageToPixels (image);
        int numRows = pixels.length;
        int numCols = pixels[0].length;
        
        // Make the new pixels.
        int[][][] mappedPixels = new int [numRows][numCols][4];

        // Almost like a copy.
        for (int i=0; i < numRows; i++) {
            for (int j=0; j < numCols; j++) {
                mappedPixels[i][j][0] = pixels[i][j][0];
                // Multiply by "a" and add "b".
                mappedPixels[i][j][1] = enforcePixelBounds (a * pixels[i][j][1] + b);
                mappedPixels[i][j][2] = enforcePixelBounds (a * pixels[i][j][2] + b);
                mappedPixels[i][j][3] = enforcePixelBounds (a * pixels[i][j][3] + b);
            }
        }

        // Returned transformed image.
        Image mappedImage = imTool.pixelsToImage (mappedPixels);
        return mappedImage;
    }
    

    // Each pixel value needs to be between 0 and 255.

    static int enforcePixelBounds (double pixelValue)
    {
        int value = (int) pixelValue;
        if (value < 0) {
            return 0;
        }
        if (value > 255) {
            return 255;
        }
        return value;
    }

}

In-Class Exercise 5: Download the above program and this image and try the following:

  • Set b=0 and try different values of a to explore how contrast works.
  • Set a=1 and try different values of b to explore how brightness works.
  • You can make the "negative" of an image by subtracting each pixel value from 255. Choose values of a and b to make this happen and demonstrate that your "negative" is working.
Can you identify the person in the image?


Odds and ends: arrays as parameters

Recall that Java only supports pass-by-value parameters, as this example shows:

public class Parameters {

    public static void main (String[] argv)
    {
        int a = 1;
        int b = 2;
        System.out.println ("BEFORE: a=" + a + " b=" + b);

        swap (a, b);
        System.out.println ("AFTER:  a=" + a + " b=" + b);
    }

    static void swap (int x, int y)
    {
        System.out.println ("  BEFORE: x=" + x + " y=" + y);
        int temp = x;
        x = y;
        y = temp;
        System.out.println ("  AFTER:  x=" + x + " y=" + y);
    }

}
Note:
  • A copy of a and b get sent into swap().

  • The value in a is copied into the parameter variable x.
  • Similarly, the value in b is copied into the parameter variable y.

  • Thus, the output is:
    BEFORE: a=1 b=2
      BEFORE: x=1 y=2
      AFTER:  x=2 y=1
    AFTER:  a=1 b=2
        

  • The values of x and y do actually get swapped.
         ⇒ But this has no effect on a and b.

Now let's consider the same question for arrays, with this example:

import java.util.*;

public class Parameters2 {

    public static void main (String[] argv)
    {
        int[] A = {1, 2};
        int[] B = {3, 4};
        
        System.out.println ("BEFORE: A=" + Arrays.toString(A) + " B=" + Arrays.toString(B));

        swap (A, B);
        System.out.println ("AFTER:  A=" + Arrays.toString(A) + " B=" + Arrays.toString(B));
    }

    static void swap (int[] X, int[] Y)
    {
        System.out.println ("  BEFORE: X=" + Arrays.toString(X) + " Y=" + Arrays.toString(Y));
        int[] temp = X;
        X = Y;
        Y = temp;
        System.out.println ("  AFTER:  X=" + Arrays.toString(X) + " Y=" + Arrays.toString(Y));
    }

}

In-Class Exercise 6: What is the output? Explain.

Next, consider this variation:

public class Parameters3 {

    public static void main (String[] argv)
    {
        int[] A = {1, 2};
        int[] B = {3, 4};
        
        System.out.println ("BEFORE: A=" + Arrays.toString(A) + " B=" + Arrays.toString(B));

        swap (A, B);
        System.out.println ("AFTER:  A=" + Arrays.toString(A) + " B=" + Arrays.toString(B));
    }

    static void swap (int[] X, int[] Y)
    {
        System.out.println ("  BEFORE: X=" + Arrays.toString(X) + " Y=" + Arrays.toString(Y));
        for (int i=0; i < X.length; i++) {
            int temp = X[i];
            X[i] = Y[i];
            Y[i] = temp;
        }
        System.out.println ("  AFTER:  X=" + Arrays.toString(X) + " Y=" + Arrays.toString(Y));
    }

}

In-Class Exercise 7: What is the output? Explain.


Shallow and Deep Copy

The pointer copy or the wrong way to copy an array:

import java.util.*;

public class PointerCopy {

    public static void main (String[] argv)
    {
        int[] A = {1, 2, 3};
        System.out.println ("A: " + Arrays.toString(A));

        // Declare a new array variable.
        int[] B;

        // Pointer copy!
        B = A;

        // A modification of B affects A
        B[1] = 5;
        System.out.println ("A: " + Arrays.toString(A));
    }

}

The value copy or the right way to copy the array:

import java.util.*;

public class ValueCopy {

    public static void main (String[] argv)
    {
        int[] A = {1, 2, 3};
        System.out.println ("A: " + Arrays.toString(A));

        int[] B;

        // First make the space - getting the length from A.
        B = new int [A.length];

        // Copy each entry.
        for (int i=0; i < A.length; i++) {
            B[i] = A[i];
        }

        // A modification of B does not affect A.
        B[1] = 5;
        System.out.println ("A: " + Arrays.toString(A));
    }

}

  • A pointer copy may also be called a shallow copy. This terminology comes from copying data at a superficial level. With multidimensional arrays and other objects in Java, we can perform a shallow copy if we copy references.

  • A value copy may also be called a deep copy. This terminology comes from having to go as deep as possible into the data structure and to visit each element during the copy.

  • We sometimes invent other terms to differentiate between these levels of copy for the convenience of naming. For example, we might call a function clone to indicate that it is a deep copy rather than some other level of copy.

In-Class Exercise 8: Download ArrayCopy.java and implement array copying for 2D arrays.


© 2006-2020, Rahul Simha & James Taylor (revised 2020)