CompSci.rocks
FRQcsapa

GrayImage: 2012 FRQ 4

A step-by-step solution to the 2012 AP CSA FRQ 4 (GrayImage), covering counting matching values in a 2D array and safely processing every cell against a diagonally offset neighbor in Java.

A grayscale picture stored as nothing more than a grid of brightness numbers anchors this AP Computer Science A free-response question — first you'll count how many pixels are pure white, then rewrite every pixel by comparing it against a neighbor sitting diagonally two rows and two columns away.

What This FRQ Tests

  • AP CSA units: Unit 8 (2D Array) and Unit 4 (Iteration)
  • Core skill: a nested loop that visits every cell of a 2D array in row-major order
  • Secondary skill: checking array bounds before reading an offset index, and clamping a computed result so it never drops below a minimum value
  • Official category: "2D Array" — this happens to land in the FRQ 4 slot 2D Array questions occupy on today's exams as well, though 2012 predates the 2019–2020 redesign that standardized that fixed FRQ 1–4 category order, so the match is coincidental rather than guaranteed.

The Setup

  • The GrayImage class has:
    • public static final int BLACK = 0 and public static final int WHITE = 255
    • private int[][] pixelValues — guaranteed not null, rectangular, with every value already inside [BLACK, WHITE]
  • You're asked to write two unrelated methods:
    • countWhitePixels() — returns how many pixels currently equal WHITE, without changing the image
    • processImage() — for every pixel, if a pixel exists two rows down and two columns over, subtract that pixel's value from the current one (clamped so it never drops below BLACK); pixels with no such neighbor are left unchanged

Part (a): Writing countWhitePixels()

The Rule, Broken Down

  1. Look at every single pixel in the 2D array — every row, every column.
  2. Count it if its value is exactly WHITE (255).
  3. The image itself must be left completely unchanged (the postcondition says so explicitly).

Step-by-Step Approach

  1. Start a counter at 0.
  2. Loop over every row index of pixelValues.
  3. Inside that, loop over every column index of the current row.
  4. If that pixel equals WHITE, increment the counter.
  5. After both loops finish, return the counter.

The Code

public int countWhitePixels()
{
    int count = 0;

    for (int row = 0; row < pixelValues.length; row++)
    {
        for (int col = 0; col < pixelValues[row].length; col++)
        {
            if (pixelValues[row][col] == WHITE)
            {
                count++;
            }
        }
    }

    return count;
}

Why Each Piece Matters

  • pixelValues.length gives the number of rows; pixelValues[row].length gives the number of columns in that row. Using the row's own length (rather than assuming a fixed column count) is the safe habit for any 2D array, even though this problem guarantees the array is rectangular.
  • == WHITE, not >= WHITE — since every value is guaranteed to already be within [BLACK, WHITE], nothing can exceed WHITE, but writing the exact comparison the rule describes avoids relying on that guarantee to still get the right answer.
  • No modification anywhere in the loop body — the method only ever reads pixelValues[row][col], which is exactly what "this image has not been changed" requires.

Tracing the Example

Using the question's own 4-row, 5-column image:

Row Values White pixels in this row
0 255, 184, 178, 84, 129 1 (index 0)
1 84, 255, 255, 130, 84 2 (indexes 1, 2)
2 78, 255, 0, 0, 78 1 (index 1)
3 84, 130, 255, 130, 84 1 (index 2)

Total: 1 + 2 + 1 + 1 = 5, matching the question's stated result exactly.

Common Mistakes to Avoid

  • Mixing up row and column bounds, e.g. looping col against pixelValues.length instead of pixelValues[row].length — this happens to work when the array is square, but is wrong in general and relies on an accidental coincidence.
  • Modifying pixelValues anywhere in this method, even temporarily — the postcondition explicitly forbids changing the image here.
  • Comparing against a literal 255 instead of the named constant WHITE — it produces the same answer, but ignores a constant the class already defines specifically to avoid "magic numbers."

Part (b): Writing processImage()

The Rule, Broken Down

  1. Process every pixel in row-major order — row 0 left to right, then row 1 left to right, and so on.
  2. For the pixel at (row, col), check whether a pixel exists at (row + 2, col + 2).
  3. If it does, subtract that neighbor's value from the current pixel's value.
  4. If the result would be less than BLACK, use BLACK instead.
  5. If no pixel exists at (row + 2, col + 2) (it would fall outside the array), leave the current pixel completely unchanged.

Step-by-Step Approach

  1. Loop over every row, then every column, in that order — this is what "row-major order" means.
  2. At each (row, col), check whether row + 2 and col + 2 are both still valid indices.
  3. If they are, compute the subtraction, clamp it to BLACK if it went negative, and store the result back into pixelValues[row][col].
  4. If they aren't both valid, do nothing and move on.

The Code

public void processImage()
{
    for (int row = 0; row < pixelValues.length; row++)
    {
        for (int col = 0; col < pixelValues[row].length; col++)
        {
            if (row + 2 < pixelValues.length && col + 2 < pixelValues[row].length)
            {
                int newValue = pixelValues[row][col] - pixelValues[row + 2][col + 2];

                if (newValue < BLACK)
                {
                    newValue = BLACK;
                }

                pixelValues[row][col] = newValue;
            }
        }
    }
}

Why Each Piece Matters

  • The bounds check comes before the subtraction, using && — both row + 2 and col + 2 have to be valid indices, and short-circuit evaluation means the second condition is only checked once the first is already known to be safe.
  • It's safe to modify pixelValues in place here, unlike a typical "read the old grid, write a new one" 2D array problem. Because processing happens in row-major order and the neighbor being read is always two rows further down, that neighbor's row hasn't been reached by the loop yet — it's read in its original, unmodified state every time.
  • The clamp happens after computing newValue, but before storing it — checking pixelValues[row][col] < BLACK after already overwriting it would be checking the wrong (already-changed) value.

Tracing the Example

Using the question's own 4-row, 5-column image (only rows 0–1 and columns 0–2 can have a valid (row + 2, col + 2) neighbor in a 4×5 grid):

Pixel (row, col) Computation Result
(0, 0) 221 - pixelValues[2][2] = 221 - 0 221
(0, 1) 184 - pixelValues[2][3] = 184 - 0 184
(0, 2) 178 - pixelValues[2][4] = 178 - 78 100
(0, 3), (0, 4) col + 2 out of range unchanged: 84, 135
(1, 0) 84 - pixelValues[3][2] = 84 - 255 = -171 clamped to BLACK → 0
(1, 1) 255 - pixelValues[3][3] = 255 - 130 125
(1, 2) 255 - pixelValues[3][4] = 255 - 84 171
(1, 3), (1, 4) col + 2 out of range unchanged: 130, 84
rows 2–3 row + 2 out of range for every column entirely unchanged

The resulting grid — row 0: 221, 184, 100, 84, 135; row 1: 0, 125, 171, 130, 84; rows 2–3 unchanged — matches the question's "After Call to processImage" diagram exactly, including the pixel at (1, 0) landing on 0 rather than the negative value the raw subtraction would have produced.

Common Mistakes to Avoid

  • Subtracting in the wrong direction — the rule is "decrease the pixel at (row, col) by the value at (row + 2, col + 2)," which means pixelValues[row][col] - pixelValues[row + 2][col + 2], not the reverse.
  • Using <= instead of < in the bounds check (e.g. row + 2 <= pixelValues.length) — this reads one row or column past the end of the array and throws an ArrayIndexOutOfBoundsException.
  • Clamping with if (newValue < BLACK) written before computing newValue, or checking pixelValues[row][col] after it's already been overwritten — the comparison has to happen against the freshly computed value, before it's stored.
  • Worrying that modifying the array while looping will corrupt later reads. It doesn't here, specifically because every neighbor read is always further down in row-major order than the loop has reached — that's worth confirming explicitly rather than assuming, since it isn't true of every 2D array problem.

Notes: A Method Not on the AP CSA Quick Reference Sheet

Math.max isn't listed in the Math class section of the Java Quick Reference sheet (only abs, pow, sqrt, and random appear there), but it's completely valid Java and lets the clamping step collapse into a single line:

public void processImage()
{
    for (int row = 0; row < pixelValues.length; row++)
    {
        for (int col = 0; col < pixelValues[row].length; col++)
        {
            if (row + 2 < pixelValues.length && col + 2 < pixelValues[row].length)
            {
                pixelValues[row][col] = Math.max(BLACK, pixelValues[row][col] - pixelValues[row + 2][col + 2]);
            }
        }
    }
}
  • Math.max(BLACK, ...) returns whichever is larger — the subtraction result or BLACK itself — which is exactly what the if (newValue < BLACK) { newValue = BLACK; } block above accomplishes in two lines.
  • This isn't a case of one version being "allowed" and the other not — both are correct Java, and AP CSA graders accept either. The only real tradeoff with Math.max is not being able to look its exact behavior up on the reference sheet if you second-guess yourself mid-exam, unlike the explicit if version.

Key Takeaways

  • "Row-major order" always means the outer loop walks rows and the inner loop walks columns within that row — not the other way around.
  • Modifying a 2D array in place while looping over it is only safe once you've confirmed every value you read has already passed (or will never be reached) by the point you write to it — here, that's guaranteed because the neighbor is always two rows further down than the current loop position.
  • Clamping a computed value to a minimum (or maximum) is a one-branch if — or a single Math.max/Math.min call if you're comfortable relying on it without the reference sheet.

Related FRQs