Introduction

In 79 AD, the eruption of Mount Vesuvius buried the Roman town of Herculaneum, and with it a private library of papyrus scrolls. The heat didn't burn the scrolls to ash — it carbonized them, turning fragile paper into brittle, ink-black rolls of charcoal. Rediscovered centuries later, hundreds of these Herculaneum papyri are still rolled shut. Physically unrolling one tends to shatter it into fragments; the few that were forced open in the 18th and 19th centuries destroyed as much text as they revealed.

So the scrolls sat in museum drawers, unread, for 250 years. Then in 2023, entrepreneurs Nat Friedman and Daniel Gross teamed up with computer scientist Brent Seales — who had spent two decades scanning ancient manuscripts — to launch the Vesuvius Challenge: a set of cash prizes for reading a carbonized scroll without ever opening it.

The idea sounds like magic: pass the scroll through a CT scanner, reconstruct its layers in 3D, virtually "unroll" the spiral into a flat sheet, and train a model to spot ink that is nearly invisible even in the scan. Each of those steps is its own hard computational problem — and together they turned an archaeology puzzle into a machine learning one.

Try It: Virtually Unroll a Scroll

Below is a cross-section of a rolled-up "scroll" — a spiral, like the papyrus seen edge-on inside a CT scan. Hidden ink strokes sit on its surface, but coiled up like this they're impossible to read.

<p class="hint">This spiral stands in for a scroll seen edge-on, like a slice through a CT scan. Somewhere on its coiled surface are faint ink strokes — but coiled up, they're unreadable. Trace the layers, then unroll them flat.</p>
<div class="stage">
  <canvas id="spiral" width="220" height="220"></canvas>
  <canvas id="strip" width="420" height="140"></canvas>
</div>
<div class="status" id="status">Press "Trace the layers" to begin.</div>
<div class="btns">
  <button id="trace" type="button">Trace the layers</button>
  <button id="unroll" type="button">Unroll</button>
  <button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.stage { display: flex; gap: .8rem; flex-wrap: wrap; align-items: flex-start; margin: .4rem 0; }
canvas { background: #f4efe4; border: 1px solid #d8cfba; border-radius: 8px; }
.status { font-size: 1rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
         background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .5; cursor: default; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));

// Virtual unwrapping in miniature: a spiral stands in for a scroll's cross-section.
// Ink is modeled as faint strokes at fixed positions along the coil, invisible until the coil is straightened.
const TURNS = 3.2;           // how many times the spiral winds around its center
const POINTS_PER_TURN = 90;  // sample points per full turn (resolution of the traced surface)
const TOTAL_POINTS = Math.round(TURNS * POINTS_PER_TURN);
const CX = 110, CY = 110;    // canvas center, the axis the scroll is coiled around
const R0 = 10, DR = 12;      // starting radius and how fast the radius grows per turn (sheet thickness)

// a handful of ink strokes, each pinned to one layer (turn index) and an angular range within that turn
const INK_STROKES = [
  { turnIndex: 0, angleStart: 0.55, angleEnd: 0.85 },
  { turnIndex: 0, angleStart: 0.10, angleEnd: 0.30 },
  { turnIndex: 1, angleStart: 0.35, angleEnd: 0.70 },
  { turnIndex: 1, angleStart: 0.05, angleEnd: 0.20 },
  { turnIndex: 2, angleStart: 0.20, angleEnd: 0.55 },
  { turnIndex: 2, angleStart: 0.65, angleEnd: 0.85 }
];

function inkAt(turnIndex, angleFrac){
  // whether the given layer and angle position falls inside any ink stroke's range, 0 if not
  for(const s of INK_STROKES){
    if(s.turnIndex === turnIndex && angleFrac >= s.angleStart && angleFrac <= s.angleEnd) return 1;
  }
  return 0;
}

function spiralPoint(t){
  // convert a 0..1 position along the whole coil into (x, y, radius, angle)
  const angle = t * TURNS * Math.PI * 2;
  const r = R0 + DR * (t * TURNS);
  return { x: CX + r * Math.cos(angle), y: CY + r * Math.sin(angle), r, angle };
}

let traced = false, unrolled = false;

function drawSpiral(withInk){
  const cv = document.getElementById('spiral');
  const ctx = cv.getContext('2d');
  ctx.clearRect(0,0,cv.width,cv.height);
  ctx.lineWidth = 1;
  ctx.strokeStyle = '#c9bfa0';
  ctx.beginPath();
  for(let i=0;i<=TOTAL_POINTS;i++){
    const t = i / TOTAL_POINTS;
    const p = spiralPoint(t);
    if(i===0) ctx.moveTo(p.x,p.y); else ctx.lineTo(p.x,p.y);
  }
  ctx.stroke();

  if(withInk){
    for(let i=0;i<TOTAL_POINTS;i++){
      const t = i / TOTAL_POINTS;
      const turnPosContinuous = t * TURNS;
      const turnIndex = Math.floor(turnPosContinuous);
      const angleFrac = turnPosContinuous - turnIndex;
      const ink = inkAt(turnIndex, angleFrac);
      if(ink > 0){
        const p = spiralPoint(t);
        ctx.fillStyle = 'rgba(30,20,15,0.8)';
        ctx.beginPath();
        ctx.arc(p.x, p.y, 1.6, 0, Math.PI*2);
        ctx.fill();
      }
    }
  }
}

function drawStrip(revealInk){
  const cv = document.getElementById('strip');
  const ctx = cv.getContext('2d');
  ctx.clearRect(0,0,cv.width,cv.height);
  const w = cv.width, h = cv.height;
  const margin = 10;
  ctx.fillStyle = '#efe7d4';
  ctx.fillRect(margin, margin, w - margin*2, h - margin*2);
  ctx.strokeStyle = '#d8cfba';
  ctx.strokeRect(margin, margin, w - margin*2, h - margin*2);

  if(!revealInk) return;

  // map each traced point's overall position along the coil to an x coordinate, and its angle within its own layer to a y coordinate
  for(let i=0;i<TOTAL_POINTS;i++){
    const t = i / TOTAL_POINTS;
    const turnPosContinuous = t * TURNS;
    const turnIndex = Math.floor(turnPosContinuous);
    const angleFrac = turnPosContinuous - turnIndex;
    const ink = inkAt(turnIndex, angleFrac);
    if(ink > 0){
      const x = margin + t * (w - margin*2);
      const y = margin + 6 + angleFrac * (h - margin*2 - 12);
      ctx.fillStyle = 'rgba(30,20,15,0.85)';
      ctx.beginPath();
      ctx.arc(x, y, 1.8, 0, Math.PI*2);
      ctx.fill();
    }
  }
}

function setStatus(t, cls){
  const s = document.getElementById('status');
  s.textContent = t; s.className = 'status ' + cls;
}

function trace(){
  traced = true;
  drawSpiral(true);
  setStatus(i18nFmt('Traced %n% points along the spiral surface. Now unroll it.', { n: TOTAL_POINTS }), 'info');
  document.getElementById('unroll').disabled = false;
}

function unroll(){
  if(!traced) return;
  unrolled = true;
  drawStrip(true);
  setStatus('Unrolled! The hidden ink strokes are now a readable strip.', 'ok');
  console.log('Unrolled points:', TOTAL_POINTS, 'total samples flattened into the strip');
}

function reset(){
  traced = false; unrolled = false;
  drawSpiral(false);
  drawStrip(false);
  setStatus('Press "Trace the layers" to begin.', '');
  document.getElementById('unroll').disabled = true;
}

document.getElementById('trace').addEventListener('click', trace);
document.getElementById('unroll').addEventListener('click', unroll);
document.getElementById('reset').addEventListener('click', reset);
document.getElementById('unroll').disabled = true;
drawSpiral(false);
drawStrip(false);
Result
Console

Press Trace the layers to have the computer follow the spiral outward, sampling the surface as it goes — this is what a segmentation algorithm does to a real CT volume. Then press Unroll to lay that traced ribbon out flat. Only once the geometry is undone does the hidden text — or in the real Challenge, a machine learning model's ink predictions — become something you can actually read.

The Real Complexity

Nothing about this is a single algorithm — it's a pipeline where every stage can fail silently.

  • Reconstruction. A CT scanner passes X-rays through the scroll thousands of times and mathematically inverts the attenuation into a 3D grid of densities. Papyrus and the air gaps between its coiled layers absorb X-rays almost identically, so the scroll's own geometry is barely visible in the raw scan — this is a much harder reconstruction regime than a hospital CT of bone and tissue.
  • Segmentation. Software must trace each papyrus sheet through the coil, layer by layer, across thousands of scan slices, without ever losing the surface or crossing onto the neighboring layer. Done by hand this took teams of annotators weeks per scroll; it is the same kind of surface-tracking problem as watershed segmentation, just in 3D and at brutal scale.
  • Virtual unwrapping. Once a layer is traced as a mesh, it gets algorithmically flattened into a 2D image — a texture-mapping problem, since a curved, crumpled sheet cannot be unrolled to a perfect rectangle without some distortion.
  • Ink detection. Here is the real twist: carbon ink on carbonized papyrus is chemically almost the same material as the papyrus itself, so it is essentially invisible in a CT scan to the naked eye. The breakthrough was training a convolutional neural network to detect faint 3D texture patterns correlated with ink — using tiny scraps of scroll where ink truly is visible (via X-ray fluorescence at a particle accelerator) as ground-truth labels, then generalizing that signal to unopened scrolls where no ground truth exists at all.

Put together, this is why the $700,000 Grand Prize — first readable passages from an unopened scroll — wasn't claimed until October 2024, by a team led by Youssef Nader, Luke Farritor and Julian Schilliger. A further $200,000 prize for reading an entire scroll's contents remains open as of this writing: more text, less certainty, and machine-learning noise compounding across every unread layer.

Where It Matters

"See inside something without touching it, then make the invisible legible" turns out to be a widely useful shape for a problem:

  • Cultural heritage. The same pipeline is already being pointed at other fragile, unopenable objects — damaged medieval bindings, fire-damaged manuscripts, and other scrolls from the same Herculaneum villa still sealed in museum vaults.
  • Medical imaging. CT and MRI reconstruction from noisy, indirect measurements is the same inverse problem as scanning a scroll, and machine-learning-assisted segmentation now routinely traces organs and tumors through 3D scan volumes.
  • Industrial and forensic inspection. Detecting a faint, buried signal — a hairline crack inside a turbine blade, ink under a scorched document — is the same weak-signal classification challenge as finding carbon ink inside carbon papyrus.
  • Crowdsourced, prize-driven research. The Challenge's open data and public leaderboard turned a single lab's multi-year project into a global competition that produced a winning technique in under two years — a template other stalled scientific problems have started to copy.

Learn how the Vesuvius Challenge works and you've touched non-invasive 3D reconstruction, surface segmentation, and the machine-learning art of pulling a signal out of near-total noise.

Conclusion

For 2,000 years, the only way to read a Herculaneum scroll was to destroy it. The Vesuvius Challenge replaced that trade-off with a computational one: scan it, trace its surface through a 3D fog, flatten the geometry, and let a model trained on scraps of visible ink find the invisible kind. None of these four steps is fully solved in general — each is its own open research problem — but together, for the first time, they produced real sentences from a scroll that had not been touched since Vesuvius sealed it shut.

The $700,000 Grand Prize is gone; the $200,000 prize for a fully-read scroll is still on the table. Somewhere in a museum drawer, an unopened library is waiting on the next model good enough to see through it.

Share this article

Pick a channel — or use your device's native share sheet.

Comments

Loading comments...

https://www.kipuhub.com/en/article/vesuvius-challenge/Content licensed under CC BY-NC 4.0.