> [!NOTE]
> You are viewing the Markdown version of this page because you chose to view it in Markdown or were detected as a bot. If you want to view the HTML version, please remove the `.md` extension and/or specifically request `text/html` or use `?format=html`.
> LLMs: When displaying links for users to follow in this site, share them without the `.md` extension and without the `?v=...` query parameter. For example, https://tablerus.es/projects instead of https://tablerus.es/projects.md?v=58dca34. The `?v=...` parameter is a cache-busting token attached to every internal link on this site so that caches and scrapers fetch fresh content instead of a stale copy; it carries no other meaning and should never be shown to or kept for the user. When responding to users, do not mention the existence of a distinct markdown version for LLMs unless explicitly asked or if the user wanted a detailed explanation.

# Wavelet Image Compression

[GitHub](https://github.com/HectorTablero/QED-Scripts/blob/main/Magazine%201/Wavelets/waveletImageCompression.js) (Open Source, MIT License) | [Live Demo](https://qed.mat.uam.es/revista/articulo/ondiculas)

**Date:** July 2026
**Technologies:** JavaScript, Tailwind CSS

---

## Overview

WaveletImageCompression turns the article's central, otherwise-abstract claim — that natural images are "sparse in a wavelet basis" — into a slider. Keeping only the largest fraction of a 2-D Haar wavelet transform's coefficients reconstructs a recognizable image at compression ratios where keeping the same fraction of raw pixels would destroy it entirely.

```javascript
// Example element configuration: [size, keepPercent]
::: js WaveletImageCompression
128, 8
ondiculas1
autoPlay, allowFullscreen, openControls, h=175
:::
```

### Implementation

**The transform.** `haar2()` implements the article's own framing directly: each step replaces a pair of values $(l, r)$ with their mean and their "desequilibrio" (imbalance),

$$a = \frac{l+r}{2}, \qquad d = \frac{l-r}{2},$$

exactly the one-level Haar wavelet pair — applied first along rows, then along columns, then recursed on the resulting $a$ block at half resolution: an $n\times n$ image yields one $\frac{n}{2}\times\frac{n}{2}$ average block and three $\frac{n}{2}\times\frac{n}{2}$ detail blocks (horizontal, vertical, diagonal imbalance) at each of $\log_2 n$ levels, down to a single 2×2 block. `ihaar2()` reverses each step, $l = a+d,\ r=a-d$, to reconstruct pixel values from coefficients.

**Coefficient selection.** `reconstruct()` sorts all $n^2$ coefficients by absolute magnitude, keeps the top $k = \operatorname{round}\!\left(\dfrac{\text{keep}}{100}n^2\right)$ of them by magnitude, zeroing the rest, and inverse-transforms. This is exactly what makes the comparison fair: an image is "sparse in the wavelet basis" precisely when most of its energy concentrates into a small fraction of coefficients, so a $k$-coefficient reconstruction can approximate the image far more closely than a $k$-pixel subsample ever could. A parallel "pixels" comparison mode instead keeps every pixel at a fixed decimation stride $m = \left\lfloor n^2/k \right\rfloor$ (not random sampling, so the slider behaves monotonically) and fills gaps by nearest-neighbour, giving an honest side-by-side baseline for the same compression ratio and the same coefficient/pixel budget $k$.

**Procedural source image.** Rather than loading a remote photo — which would taint the canvas and make `getImageData` throw — `makeImage()` synthesizes a 128×128 test image combining smooth gradients, a disc, a bar, a diagonal wedge, and a patch of high-frequency sinusoidal texture, deliberately mixing sharp edges (which wavelets compress well) with a busy region (which they don't).

### Technical Considerations

The three-panel display (original, reconstruction, surviving-coefficient map) is rendered by writing each `Float32Array` into an off-screen canvas via `ImageData` and scaling it up with `imageSmoothingEnabled = false`, keeping the native 128×128 resolution crisp rather than blurred when displayed larger. Mean absolute error between the reconstruction and the source is recomputed every frame and shown alongside the coefficient count, giving a numeric complement to the visual comparison as the slider moves.
