Skip to content

feat(resample): resample_oriented, the upright result without the upright copy - #2

Merged
oddharsh merged 1 commit into
mainfrom
resample-oriented
Sep 22, 2026
Merged

oddharsh merged 1 commit into
mainfrom
resample-oriented

Conversation

@oddharsh

@oddharsh oddharsh commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

A camera stores most portraits sideways, so a photo pipeline orients a full frame before every resample: a copy of the whole frame (311 MB of f32 for a 26 MP RGB photo), and for the four orientations that swap axes, a copy that reads its source down a column.

This adds:

  • Orientation: the eight EXIF transforms, with from_exif, swaps_axes, upright_size and source_of.
  • orient: the per-pixel permutation, public, and the definition the tests hold the fast path to.
  • resample_oriented: returns resample(&orient(..), ..) bit for bit without building the upright frame.

Why it's exact

Every accumulator sums the same samples, with the same weights, taps ascending. Only the loops around the accumulators change. For the axis-swapping orientations an upright row is a stored column, so every upright row accumulates at once and each tap streams one whole stored row. The vertical pass is now one shared function, so resample and resample_oriented can't drift.

Measured

6240x4160 RGB, three box tiers (600/400/200 px short edge), release, medians of 7:

upright input resample_oriented tiled copy, then resample
EXIF 6 59 ms 46 ms 129 ms
EXIF 8 59 ms 61 ms 149 ms

It started blocked: 64 upright rows at a time took 174/240 ms. Every doubling helped until one block spanned the frame, so the block was deleted. Streaming whole rows matches or beats resampling an already-upright frame.

Tests

  • resample_oriented against orient-then-resample, to_bits() equality, over all 8 orientations, all 3 filters, channel counts 1/3/4 (fixed and dynamic paths) and 8 frame sizes down to 1x1. Mutation controls: it fails on a wrong horizontal flag, a wrong vertical flag, and an unreversed mirrored walk.
  • orient pinned by hand on a 3x2 frame.
  • cargo fmt --check, clippy -D warnings on the pinned 1.98, MSRV 1.75 check, conformance: all pass.

First consumer

aadhar.sh's zenc pins this branch commit (871487e) in oddharsh/site#891. Please merge with a merge commit, not a squash, as #1 was, so that sha stays reachable from main.

The wasm/JS surface is unchanged; resample_oriented isn't exported there yet.

…ight copy

Adds Orientation (the eight EXIF transforms), orient (the per-pixel
definition), and resample_oriented, which returns resample(&orient(..), ..)
bit for bit without building the upright frame.

Equality is by construction: every accumulator sums the same samples, with the
same weights, taps ascending. Only the loops around the accumulators change.
For the four axis-swapping orientations an upright row is a stored column, so
every upright row accumulates at once and each tap streams one stored row.

Measured on a 6240x4160 RGB frame, three box tiers, release, medians of 7:
blocks of 64 upright rows took 174/240 ms at EXIF 6/8, improving with every
doubling until one block spanned the frame at 46-62 ms. Resampling an
already-upright frame takes 59 ms; a tiled copy then resample, 129/149 ms.

Tests: resample_oriented against orient-then-resample, bitwise, over every
orientation, all three filters, channel counts 1/3/4 and eight frame sizes
down to 1x1. It fails on a wrong horizontal flag, a wrong vertical flag, and
an unreversed mirrored walk. orient is pinned by hand on a 3x2 frame.
fmt, clippy -D warnings (1.98), MSRV 1.75 check, conformance all pass.
@oddharsh
oddharsh merged commit f9ac799 into main Sep 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant