Task 5 of 6

Payoff: Thirty-Two Seams

One seam is a curiosity. Thirty-two is a resize. And the loop has a catch that matters: once a seam is gone the picture is a different picture, so the energy map and the cost map both have to be built again from the carved luminance — not from the original. Energy → cost → seam → carve, and round again.

Everything you wrote is here already. What is left is the orchestration, and the order is the whole thing: each stage awaits the one before it, and inside the cost map the rows must go one at a time, because row y reads row y - 1's answer. Three colour planes and the luminance plane all reflow with the same seam — the carve kernel has no idea what it is carving, which is why one kernel does all four.

Two console tricks pay for themselves here. render() once per removal collapses into a frame scrubber you can drag back and forth — that is where you actually see the picture reflow. And plotting each seam's energy against its removal number gives you the curve that tells you when to stop: the cheap seams go first, and when the curve knees upwards the picture has run out of things it can afford to lose. The slider under the console re-runs the whole program, so you can carve less and compare.

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: fill in carveOnce — energy, cumulative cost, backtrack, then carve every plane with that one seam — and return the new planes together with the seam's price.

Requirements

Hint 1 — the current width

Nothing needs to be tracked by hand: the planes know how wide they are.

const w = planes[0][0].length;
energy.setOutput([w, 72]);
step.setOutput([w]);
carve.setOutput([w - 1, 72]);
Hint 2 — the cost map, again

Exactly the driver from the cumulative-cost task, over the energy map you just built:

const rows = [Float32Array.from(e[0])];
for (let y = 1; y < 72; y++) rows.push(await step(e[y], rows[y - 1]));
Hint 3 — carving four planes with one seam

A for loop, not .map() — a callback cannot hold an await, and every one of these is a kernel call:

const next = [];
for (let i = 0; i < planes.length; i++) {
  next.push(await carve(planes[i], seam));
}
return { planes: next, cost: rows[71][seam[71]] };

Same idea elsewhere

A per-frame chain of dependent passes with a host-side loop around it is what a real pipeline looks like on every platform: CUDA streams a sequence of launches, WebGPU records passes into a command encoder, Metal encodes one compute pass per stage. And the cost of this particular shape is visible in the numbers — 71 cost-map launches per removal, well over two thousand for the whole run, each one tiny. When a wavefront gets slow it is almost never the arithmetic; it is the launch count.

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.