# box-image-pyramid

Minimal grayscale image pyramid construction for the exact case this workspace
needs: repeated 2x downsampling of `u8` row-major images with a 2x2 box filter.

This crate is intentionally narrow:

- `u8` grayscale only
- fixed 2x box-filter downsampling
- reusable per-level buffers across frames
- no dependency on a full image-processing stack

It is used by the multiscale detector in `chess-corners`, but it is published as
a standalone crate because the pyramid builder is generally useful on its own.

## What It Provides

- `ImageView<'a>`: a borrowed grayscale image view over `&[u8]`
- `ImageBuffer`: an owned grayscale buffer
- `PyramidParams`: number of levels and minimum level size
- `PyramidBuffers`: reusable storage for successive pyramid builds
- `build_pyramid(...)`: build a top-down pyramid where each level is half the
  previous width and height

The base image is always level 0. Subsequent levels are generated by averaging
each 2x2 block with integer rounding:

```text
dst[x, y] = (p00 + p01 + p10 + p11 + 2) >> 2
```

## Example

```rust
use box_image_pyramid::{build_pyramid, ImageView, PyramidBuffers, PyramidParams};

let pixels = vec![128u8; 640 * 480];
let base = ImageView::new(640, 480, &pixels).unwrap();

let mut params = PyramidParams::default();
params.num_levels = 4;
params.min_size = 32;

let mut buffers = PyramidBuffers::with_capacity(params.num_levels);
let pyramid = build_pyramid(base, &params, &mut buffers);

assert_eq!(pyramid.levels.len(), 4);
assert_eq!(pyramid.levels[0].img.width, 640);
assert_eq!(pyramid.levels[1].img.width, 320);
assert_eq!(pyramid.levels[2].img.width, 160);
assert_eq!(pyramid.levels[3].img.width, 80);
```

When processing a stream of frames, keep one `PyramidBuffers` around and reuse it.
That avoids reallocating intermediate levels on every call.

## Feature Flags

- `rayon`: enables rayon support
- `simd`: enables portable-SIMD support
- `par_pyramid`: turns on the accelerated pyramid downsampling paths
- `tracing`: emits tracing spans around pyramid construction

Without `par_pyramid`, the crate uses the scalar implementation.

## Performance

The crate includes a Criterion benchmark:

```bash
cargo bench -p box-image-pyramid --bench pyramid_perf
```

The benchmark compares `box-image-pyramid` with license-compatible Rust
alternatives that can be used to build pyramids:

- `fast_image_resize` with box-filter convolution
- `resize` with a box filter
- `image::imageops::resize` with triangle filtering

Notes:

- The `image` benchmark is a familiar ecosystem baseline, not a filter-matched one.
- A dedicated `image-pyramid` crate exists, but its published docs currently list
  `GPL-3.0-only`, so it was not added as a dev-dependency in this MIT workspace.
- The benchmark measures repeated pyramid construction on deterministic grayscale
  images. It is not a general image-resizing shootout.

Example measurements from this repository's benchmark on `aarch64-apple-darwin`
with this hardware/software setup:

- Machine: MacBook Pro (`Mac16,8`)
- SoC: Apple M4 Pro
- CPU: 12 cores (8 performance + 4 efficiency)
- Memory: 24 GB
- Toolchain: `rustc 1.93.0-nightly (1be6b13be 2025-11-26)`
- Target: `aarch64-apple-darwin`

Measured timings:

| Input | `box-image-pyramid` reused buffers | `box-image-pyramid` fresh buffers | `fast_image_resize` box | `resize` box | `image` triangle |
| --- | ---: | ---: | ---: | ---: | ---: |
| 640x480 | 8.53 us | 10.72 us | 105.29 us | 320.59 us | 0.99 ms |
| 1920x1080 | 41.71 us | 46.46 us | 621.47 us | 732.46 us | 6.77 ms |
| 4032x3024 | 223.50 us | 248.51 us | 3.43 ms | 2.28 ms | 44.86 ms |

For this specific workload, `box-image-pyramid` is roughly:

- 12x to 15x faster than `fast_image_resize`
- 10x to 38x faster than `resize`
- 45x to 100x faster than the allocating `image` baseline

Reusing `PyramidBuffers` also saves about 11% to 26% versus constructing fresh
buffers on every iteration.

## Why The Gap Is So Large

The short version: this crate does much less work per output pixel than a
general-purpose resizer.

`box-image-pyramid` is specialized for one hot path only:

- one pixel type: `u8`
- one channel: grayscale
- one scale factor: exactly 1/2 in each dimension
- one filter: exact 2x2 box average
- one memory layout: dense row-major slices

That lets the inner loop collapse to:

- 4 byte loads
- 4 `u8 -> u16` promotions
- 3 additions
- 1 rounding shift
- 1 byte store

There is no coefficient table lookup, no floating-point accumulation, no
arbitrary support window, and no generic dispatch in the hot loop.

By contrast, generic resize crates usually need to support:

- arbitrary scale factors
- many filter kernels
- many pixel formats
- arbitrary strides/layouts
- consistent edge handling for the general case

Even when configured with a box filter, those crates typically still run a
generic separable resampling pipeline with coefficient application and wider
accumulators. That is a more general algorithm, but it is also more arithmetic
and more memory traffic than a fixed 2x pyramid kernel.

So the large gap is not evidence that the other crates are poor implementations.
It mainly reflects specialization:

- this crate is optimized for exactly one pyramid operation
- the others are optimized for general resizing problems

If your workload is "build a grayscale 2x pyramid every frame", this crate is a
good fit. If you need arbitrary scaling, color formats, or higher-quality
filters, the general-purpose resize crates remain the better tool.
