JavaScript · Course navigation

JavaScript: a sharpen filter and why clamping matters

Apply a sharpening kernel to a step edge and see the overshoot clamped to the valid range.

On this page
Before you start

JavaScript arrays and the convolution idea.

  • Apply a sharpening kernel to a step edge and see the overshoot clamped to the valid range.
  • Predict the sample output and explain the state changes.
  • Complete the practice task and check the boundary cases.

The concept

A sharpening kernel emphasizes the difference between a pixel and its four neighbours: 5 times the center minus the neighbours above, below, left, and right. On a flat area the weights sum to 1, so the pixel is unchanged. Near an edge it exaggerates the contrast.

At a step from 50 to 200, the dark side of the edge becomes darker than 50 and the bright side brighter than 200, a halo-like overshoot. The raw result can leave the valid 0 to 255 range, here reaching −100 and 350, so each output is clamped. Without clamping, such values would wrap around or be stored incorrectly.

Code example

JavaScript
const width = 5;
const image = [];
for (let r = 0; r < 5; r++) {
  for (let c = 0; c < width; c++) {
    image.push(c < 2 ? 50 : 200);
  }
}

const clamp = (v) => Math.min(255, Math.max(0, v));
const at = (r, c) => image[r * width + c];

for (let r = 1; r < 4; r++) {
  const row = [];
  for (let c = 1; c < 4; c++) {
    const raw = 5 * at(r, c) - at(r - 1, c) - at(r + 1, c) - at(r, c - 1) - at(r, c + 1);
    row.push(clamp(raw));
  }
  console.log(row.join(" "));
}
Output
0 255 200
0 255 200
0 255 200

Follow the code

Open the walkthrough to inspect the example one step at a time.

Show guided walkthrough
Guided walkthroughReady
1const width = 5;
2const image = [];
3for (let r = 0; r < 5; r++) {
4 for (let c = 0; c < width; c++) {
5 image.push(c < 2 ? 50 : 200);
6 }
7}
8
9const clamp = (v) => Math.min(255, Math.max(0, v));
10const at = (r, c) => image[r * width + c];
11
12for (let r = 1; r < 4; r++) {
13 const row = [];
14 for (let c = 1; c < 4; c++) {
15 const raw = 5 * at(r, c) - at(r - 1, c) - at(r + 1, c) - at(r, c - 1) - at(r, c + 1);
16 row.push(clamp(raw));
17 }
18 console.log(row.join(" "));
19}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Columns 0 and 1 are dark (50) and columns 2 to 4 are bright (200): a vertical step edge.—
—
At column 1 the raw value is 5·50 − (50+50+50+200) = −100. At column 2 it is 5·200 − (200+200+50+200) = 350. At column 3 it is 200.raw = −100, 350, 200
—
Clamp into 0 to 255: −100 becomes 0, 350 becomes 255, and 200 is already valid.clamped = 0, 255, 200
—
Same result on every row, because the image is identical down each column.clamped = 0, 255, 200
0 255 200
0 255 200
0 255 200

Behind the scenes

How JavaScript processes this code

A JavaScript engine parses and runs the source. A function body runs when the function is called. console.log writes to the console.

Cost and common mistakes

O(p) for p pixels with a fixed 3×3 kernel.

  • Sharpening also amplifies noise. A light blur before sharpening, or unsharp masking, gives cleaner results.
  • Applying the kernel in place overwrites pixels that later neighbours still need. Write results to a separate array.

Try it yourself

What would the raw value be at a flat pixel where the pixel and all four neighbours equal 100?

Reveal answer and reasoning

5·100 − 4·100 = 100, so flat regions are unchanged.

Further reading