Task 1 of 6

What Can We Afford to Lose?

To make a picture narrower you can squash it, crop it — or delete the pixels nobody would miss. Seam carving does the third: it removes a seam, a connected path of one pixel per row, threaded through the least interesting part of the picture, and does it once per column you want to lose. Everything then slides across to close the gap, so the interesting parts keep their proportions and the boring parts get squeezed out.

"Interesting" needs a number, and the usual one is the gradient magnitude: flat regions score near zero, edges score high. That is exactly the Sobel pass Convolution & Filters already derives — the two weight grids are in the starter and we are not deriving them again. What changes here is two small things, and both matter later:

The energy must be defined at the border. A seam is allowed to run straight down the edge of the picture, so column 0 needs a price like every other column — clamp the neighbour coordinates instead of painting the border black. And the kernel must work at any width, because after the first seam comes out the picture is 127 columns wide, then 126… so the size comes from this.output.x and this.output.y, never from a constant.

Array layout in gpu.js

Image data comes in row-major: image[y][x] is the pixel in row y, column x, and each pixel is an [r, g, b, a] array with channels from 0 to 1. Mind the inversion that catches everyone — sizes are given width-first (output: [width, height]), but indexing runs row-first, so this thread's own pixel is image[this.thread.y][this.thread.x]. Swap those two and you read the transpose of your image. Three-dimensional data follows the same rule: output: [w, h, d] is indexed [z][y][x].

Goal: finish energy so that cell [y][x] holds Math.sqrt(gx * gx + gy * gy) for the 3×3 neighbourhood of gray, with every neighbour coordinate clamped to the picture.

Requirements

Hint 1 — the clamp

Four one-liners, and they are the only thing standing between you and a NaN in the first and last row and column:

const xm = Math.max(x - 1, 0);
const xp = Math.min(x + 1, this.output.x - 1);

— and the same pair on y, against this.output.y - 1. A pixel on the edge now simply sees itself twice, which is what "replicate the border" means.

Hint 2 — the two sums

Read them straight off the grids in the starter — right column minus left, middle row counted double:

const gx = (gray[ym][xp] + 2 * gray[y][xp] + gray[yp][xp])
         - (gray[ym][xm] + 2 * gray[y][xm] + gray[yp][xm]);

gy is the same move on rows: bottom row minus top row, middle column counted double.

Hint 3 — why not a constant

this.output.x is the width of this launch, not of the original picture. Hard-code 127 and the kernel is right exactly once — the first time — and then quietly clamps to a column that no longer exists.

Same idea elsewhere

Every content-aware tool starts by building a cost field and only then decides what to do with it. NVIDIA's NPP and OpenCV's CUDA module both ship Sobel as a primitive; a video encoder builds the same gradient field to decide which macroblocks deserve bits; and "saliency map first, decision second" is the shape of seam carving, content-aware fill and adaptive sampling alike.

All tasks in Seam Carving: Content-Aware Resizing

  1. What Can We Afford to Lose?
  2. The Cheapest Path Down: One Launch per Row
  3. Reading the Seam Back Out
  4. Take It Out, Let the Picture Close Up
  5. Payoff: Thirty-Two Seams
  6. What It Does Badly, and the Fix Everybody Ships

This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.