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
GrayImageclass has:public static final int BLACK = 0andpublic static final int WHITE = 255private int[][] pixelValues— guaranteed notnull, rectangular, with every value already inside[BLACK, WHITE]
- You're asked to write two unrelated methods:
countWhitePixels()— returns how many pixels currently equalWHITE, without changing the imageprocessImage()— 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 belowBLACK); pixels with no such neighbor are left unchanged
Part (a): Writing countWhitePixels()
The Rule, Broken Down
- Look at every single pixel in the 2D array — every row, every column.
- Count it if its value is exactly
WHITE(255). - The image itself must be left completely unchanged (the postcondition says so explicitly).
Step-by-Step Approach
- Start a counter at
0. - Loop over every row index of
pixelValues. - Inside that, loop over every column index of the current row.
- If that pixel equals
WHITE, increment the counter. - 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.lengthgives the number of rows;pixelValues[row].lengthgives 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 exceedWHITE, 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
colagainstpixelValues.lengthinstead ofpixelValues[row].length— this happens to work when the array is square, but is wrong in general and relies on an accidental coincidence. - Modifying
pixelValuesanywhere in this method, even temporarily — the postcondition explicitly forbids changing the image here. - Comparing against a literal
255instead of the named constantWHITE— 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
- Process every pixel in row-major order — row 0 left to right, then row 1 left to right, and so on.
- For the pixel at
(row, col), check whether a pixel exists at(row + 2, col + 2). - If it does, subtract that neighbor's value from the current pixel's value.
- If the result would be less than
BLACK, useBLACKinstead. - 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
- Loop over every row, then every column, in that order — this is what "row-major order" means.
- At each
(row, col), check whetherrow + 2andcol + 2are both still valid indices. - If they are, compute the subtraction, clamp it to
BLACKif it went negative, and store the result back intopixelValues[row][col]. - 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
&&— bothrow + 2andcol + 2have 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
pixelValuesin 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 — checkingpixelValues[row][col] < BLACKafter 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 meanspixelValues[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 anArrayIndexOutOfBoundsException. - Clamping with
if (newValue < BLACK)written before computingnewValue, or checkingpixelValues[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 orBLACKitself — which is exactly what theif (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.maxis not being able to look its exact behavior up on the reference sheet if you second-guess yourself mid-exam, unlike the explicitifversion.
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 singleMath.max/Math.mincall if you're comfortable relying on it without the reference sheet.