Task 1 of 6
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.
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].
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.Math.max(x - 1, 0) and Math.min(x + 1, this.output.x - 1), the same on y — so the border has an energy rather than a holegx (right column minus left) and gy (bottom row minus top) with the two Sobel grids in the starterMath.sqrt(gx * gx + gy * gy)dynamicOutput: true and dynamicArguments: true — the same kernel runs at every width the picture passes throughFour 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.
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.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.