Introduction

Every time you share a photo, open a webpage, or video-call someone, a piece of mathematics called the Discrete Cosine Transform is silently at work. JPEG โ€” the format behind most photos on the internet โ€” uses it to shrink a raw image by a factor of ten or more while keeping the result indistinguishable from the original to the human eye.

The core idea is disarmingly simple: not all image information is equally visible. The fine-grained, rapidly oscillating patterns โ€” the high-frequency detail โ€” are things our eyes struggle to see at normal viewing distances. Throw those away, and you barely notice. Keep the broad strokes โ€” the low-frequency shapes and gentle gradients โ€” and the image still looks sharp.

JPEG was standardized in 1992 by the Joint Photographic Experts Group, and the algorithm at its heart has not changed. Understanding it means understanding one of the most successful ideas in all of applied mathematics: transform a problem into a space where you can separate the important from the expendable, then discard the rest.

Try It

The slider below controls how many DCT coefficients are kept from each 8ร—8 block โ€” low numbers keep only the lowest-frequency information, high numbers keep almost everything. Watch how image quality and the retained data change.

<div class="controls">
  <label for="quality">Kept coefficients: <strong id="qualLabel">10</strong> / 64</label>
  <input type="range" id="quality" min="1" max="64" value="10">
  <span class="size-note" id="sizeNote"></span>
</div>
<div class="canvases">
  <div class="canvas-wrap">
    <div class="caption">Original (8ร—8 block)</div>
    <canvas id="origCanvas" width="128" height="128"></canvas>
  </div>
  <div class="canvas-wrap">
    <div class="caption">After DCT + keep <span id="keepLabel">10</span> coeffs</div>
    <canvas id="dctCanvas" width="128" height="128"></canvas>
  </div>
  <div class="canvas-wrap">
    <div class="caption">DCT coefficients (log scale)</div>
    <canvas id="coeffCanvas" width="128" height="128"></canvas>
  </div>
</div>
<div class="info" id="info"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-direction: column; gap: .4rem; margin-bottom: .8rem; }
label { font-size: .9rem; color: #333; }
input[type=range] { width: 100%; max-width: 340px; accent-color: #1d3557; }
.size-note { font-size: .8rem; color: #556; }
.canvases { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: .6rem; }
.canvas-wrap { display: flex; flex-direction: column; align-items: center; gap: 4px; }
.caption { font-size: .78rem; color: #445; text-align: center; max-width: 128px; }
canvas { border: 1px solid #cdd9e3; border-radius: 6px; image-rendering: pixelated; }
.info { font-size: .85rem; color: #1d3557; min-height: 1.4em; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// JPEG-DCT demo: apply a real 2D DCT to an 8x8 synthetic "image" block,
// zero out the high-frequency coefficients (zig-zag order), reconstruct,
// and display original vs reconstructed vs coefficient heatmap.

const N = 8;

// โ”€โ”€ Synthetic 8ร—8 luminance block (0-255) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// A plausible natural-image block: gentle gradient + a bright spot.
const ORIG = [
  [120,122,124,126,128,130,132,134],
  [118,130,135,128,126,128,130,132],
  [116,128,150,145,130,126,128,130],
  [114,126,145,160,150,135,126,128],
  [112,124,130,150,155,148,130,126],
  [110,122,126,135,148,145,128,124],
  [108,120,124,126,130,128,126,122],
  [106,118,120,122,124,122,120,118]
];

// โ”€โ”€ 1D DCT-II (orthonormal) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function dct1d(x) {
  const n = x.length;
  const out = new Array(n);
  for (let k = 0; k < n; k++) {
    let sum = 0;
    for (let i = 0; i < n; i++) sum += x[i] * Math.cos(Math.PI * k * (2*i+1) / (2*n));
    out[k] = sum * (k === 0 ? Math.sqrt(1/n) : Math.sqrt(2/n));
  }
  return out;
}

// โ”€โ”€ 2D DCT via separable 1D passes โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function dct2d(block) {
  const n = block.length;
  // rows
  let tmp = block.map(row => dct1d(row));
  // columns
  const out = Array.from({length: n}, () => new Array(n));
  for (let c = 0; c < n; c++) {
    const col = tmp.map(row => row[c]);
    const dc = dct1d(col);
    for (let r = 0; r < n; r++) out[r][c] = dc[r];
  }
  return out;
}

// โ”€โ”€ Inverse 1D DCT-III โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function idct1d(X) {
  const n = X.length;
  const out = new Array(n);
  for (let i = 0; i < n; i++) {
    let sum = X[0] * Math.sqrt(1/n);
    for (let k = 1; k < n; k++)
      sum += X[k] * Math.sqrt(2/n) * Math.cos(Math.PI * k * (2*i+1) / (2*n));
    out[i] = sum;
  }
  return out;
}

function idct2d(C) {
  const n = C.length;
  // columns first
  let tmp = Array.from({length: n}, () => new Array(n));
  for (let c = 0; c < n; c++) {
    const col = C.map(row => row[c]);
    const ic = idct1d(col);
    for (let r = 0; r < n; r++) tmp[r][c] = ic[r];
  }
  // rows
  return tmp.map(row => idct1d(row));
}

// โ”€โ”€ Zig-zag scan order for 8ร—8 โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function zigzag8() {
  const order = [];
  let r = 0, c = 0, going_up = true;
  while (order.length < 64) {
    order.push([r, c]);
    if (going_up) {
      if (c === N-1)      { r++;  going_up = false; }
      else if (r === 0)   { c++;  going_up = false; }
      else                { r--; c++; }
    } else {
      if (r === N-1)      { c++;  going_up = true; }
      else if (c === 0)   { r++;  going_up = true; }
      else                { r++; c--; }
    }
  }
  return order;
}
const ZZ = zigzag8();

// โ”€โ”€ Rendering helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
function drawBlock(canvas, block, clamp) {
  const ctx = canvas.getContext('2d');
  const pw = canvas.width / N;
  for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
    let v = Math.round(block[r][c]);
    if (clamp) v = Math.max(0, Math.min(255, v));
    ctx.fillStyle = `rgb(${v},${v},${v})`;
    ctx.fillRect(c * pw, r * pw, pw, pw);
  }
}

function drawCoefficients(canvas, C) {
  const ctx = canvas.getContext('2d');
  const pw = canvas.width / N;
  // find max abs for normalization
  let maxAbs = 0;
  for (let r = 0; r < N; r++) for (let c = 0; c < N; c++)
    maxAbs = Math.max(maxAbs, Math.abs(C[r][c]));
  for (let r = 0; r < N; r++) for (let c = 0; c < N; c++) {
    const v = C[r][c];
    // log-scale magnitude, color: red=positive, blue=negative
    const mag = Math.log1p(Math.abs(v)) / Math.log1p(maxAbs);
    const intensity = Math.round(mag * 255);
    if (v >= 0) ctx.fillStyle = `rgb(${intensity},${Math.round(intensity*0.4)},0)`;
    else        ctx.fillStyle = `rgb(0,${Math.round(intensity*0.4)},${intensity})`;
    ctx.fillRect(c * pw, r * pw, pw, pw);
  }
}

// โ”€โ”€ Main update โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const dctFull = dct2d(ORIG);

function update(kept) {
  // Zero out everything beyond the first `kept` zig-zag positions
  const C = Array.from({length: N}, () => new Array(N).fill(0));
  for (let i = 0; i < kept; i++) {
    const [r, c] = ZZ[i];
    C[r][c] = dctFull[r][c];
  }

  // Reconstruct
  const rec = idct2d(C);

  // Draw
  drawBlock(document.getElementById('origCanvas'), ORIG, false);
  drawBlock(document.getElementById('dctCanvas'), rec, true);
  drawCoefficients(document.getElementById('coeffCanvas'), C);

  // Compute MSE
  let mse = 0;
  for (let r = 0; r < N; r++) for (let cc = 0; cc < N; cc++) {
    const diff = ORIG[r][cc] - Math.max(0, Math.min(255, rec[r][cc]));
    mse += diff * diff;
  }
  mse /= (N * N);
  const psnr = mse < 0.0001 ? Infinity : 10 * Math.log10(255*255 / mse);

  const pct = Math.round(kept / 64 * 100);
  document.getElementById('info').textContent =
    fmt('Keeping %pct%% % of coefficients. MSE = %mse%. PSNR = %v%.', { pct: pct, mse: mse.toFixed(2), v: isFinite(psnr) ? psnr.toFixed(1) + ' dB' : 'โˆž' });
  document.getElementById('sizeNote').textContent =
    fmt('(Discarding %v% of 64 coefficients โ€” %v2%% % of frequency data thrown away)', { v: 64 - kept, v2: 100 - pct });

  console.assert(kept === 64 ? mse < 0.01 : true, 'Full reconstruction should be lossless');
}

const slider = document.getElementById('quality');
const qualLabel = document.getElementById('qualLabel');
const keepLabel = document.getElementById('keepLabel');

slider.addEventListener('input', () => {
  const k = parseInt(slider.value);
  qualLabel.textContent = k;
  keepLabel.textContent = k;
  update(k);
});

update(parseInt(slider.value));

// โ”€โ”€ Node.js verification (ignored in browser) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
if (typeof module !== 'undefined') module.exports = { dct2d, idct2d, ZZ, ORIG };
Result
Console

Notice that even at very low quality the broad shapes survive. The high-frequency coefficients โ€” which encode sharp edges and fine textures โ€” are the first to go. At mid quality the image already looks acceptable; the data savings are enormous. This is the essential trade-off JPEG makes: bet on what human vision cannot resolve, and be right almost every time.

The Real Complexity

Beneath the slider is a precise algorithm running in well-understood time.

  • The DCT formula. For an 8ร—8 block of pixel values, the DCT computes 64 coefficients. Coefficient (u, v) captures how much of the cosine wave with horizontal frequency u and vertical frequency v is present in the block. Low (u, v) = slow oscillation = broad shapes. High (u, v) = fast oscillation = fine texture.
  • Complexity. A naive DCT on an n-point signal is O(n2)O(n^{2}). With the Fast Fourier Transform relationship, it can be reduced to O(nlogโกn)O(n \log n) โ€” the same family as sorting. For the fixed 8ร—8 block, this is just 64 constants regardless of image size.
  • Quantization. After the DCT, each coefficient is divided by a number from a quantization table and rounded to the nearest integer. This is where information is actually thrown away. High-frequency slots get large divisors (coarse rounding = heavy loss). Low-frequency slots get small divisors (fine rounding = light loss). The quality setting you choose adjusts these divisors.
  • Entropy coding. The quantized values โ€” mostly zeros at low quality โ€” are then losslessly compressed with Huffman codes, squeezing out the remaining redundancy.
  • Why it works. Natural images have most of their energy in low frequencies. The DCT "compacts" this energy into the first few coefficients โ€” an empirical fact known as energy compaction โ€” which is why aggressive truncation of the rest is nearly invisible.

The whole standard is proven to be optimal among transform coders for natural images with Gaussian statistics. It is one of the few cases where the theoretically best answer and the practically deployed one are the same thing.

Where It Matters

The DCT is one of the most deployed algorithms in human history:

  • Digital photography. Every camera's "JPEG quality" setting is controlling the quantization table. The majority of photos ever taken are stored using this algorithm.
  • Video compression. MPEG-1, MPEG-2 (DVD), H.264 (streaming, Blu-ray), and H.265 all use DCT-based transforms on macroblocks. The video call you are on right now almost certainly uses it.
  • Audio compression. MP3 and AAC apply a closely related Modified DCT (MDCT) to audio frames โ€” the same principle: transform, quantize the inaudible, compress.
  • Medical imaging. DICOM, the format for CT and MRI scans, can use JPEG compression for preview images and progressive transmission.
  • WebP and HEIC. Newer formats use more sophisticated transforms (HEIC uses HEVC's integer DCT) but the conceptual lineage goes straight back to JPEG's 8ร—8 block idea.

The transform also appears in dimensionality reduction and signal processing far beyond images: any time you want to represent a signal compactly, the frequency domain is the first place to look.

Conclusion

JPEG is a beautiful example of mathematics meeting biology. The Discrete Cosine Transform does not know about eyes โ€” it only computes frequencies. But by pairing it with a quantization table tuned to human perception, engineers in 1992 created an algorithm that is simultaneously theoretically optimal and practically invisible.

The key insight โ€” transform first, then throw away what cannot be seen โ€” reappears in every modern codec, every audio format, and every image standard that followed. The numbers change; the principle does not.

The next time you share a photo and marvel that it fits in a few hundred kilobytes, remember: somewhere in that file are 64 cosine waves per block, with most of their amplitudes quietly set to zero. You don't see the loss, and that is the entire point. For related ideas, explore dimensionality reduction or sorting lower bounds.

Share this article

Pick a channel โ€” or use your device's native share sheet.

Comments

Loading comments...

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