resamplescope-rs [](https://github.com/imazen/resamplescope-rs/actions/workflows/ci.yml) [](https://crates.io/crates/resamplescope-rs) [](https://lib.rs/crates/resamplescope-rs) [](https://docs.rs/resamplescope-rs) [](https://doc.rust-lang.org/cargo/reference/manifest.html#the-rust-version-field) [](#license)
June 28, 2026 · View on GitHub
resamplescope-rs reverse-engineers the resampling filter behind any image resizer. Hand it a resize closure and it reconstructs the filter kernel's shape, names the closest match (Box, Triangle, Hermite, Catmull-Rom, Mitchell, B-Spline, Lanczos2/3/4), reports correlation and support radius, detects the edge-handling mode, and renders a scope graph. A Rust port of Jason Summers' ResampleScope, trading the original's file-based round-trip for an in-memory callback API. Pure Rust, #![forbid(unsafe_code)], no image-codec dependencies.
Quick start
The public API hands you imgref and rgb types directly: your resize closure takes an ImgRef<'_, u8> and returns an ImgVec<u8>, and graphs come back as ImgVec<RGB8>. Add both alongside this crate, matching the versions it builds against:
[dependencies]
resamplescope-rs = "0.1.0"
imgref = "1"
rgb = { version = "0.8", default-features = false }
use imgref::{ImgRef, ImgVec};
use resamplescope::{analyze, AnalysisConfig};
// Wrap your resizer so it takes a grayscale source and target dimensions.
fn my_resize(src: ImgRef<'_, u8>, dst_w: usize, dst_h: usize) -> ImgVec<u8> {
// ...call your real resizer here, returning a dst_w x dst_h grayscale image...
todo!()
}
let result = analyze(&my_resize, &AnalysisConfig::default()).unwrap();
// best_match() names the filter only when correlation > 0.99.
if let Some(best) = result.best_match() {
println!("Detected filter: {} (r={:.4})", best.filter, best.correlation);
}
// Render a 600x300 scope graph as ImgVec<RGB8> (encode it with your own codec).
let graph = result.render_graph();
The package is resamplescope-rs; the library is resamplescope, so imports read use resamplescope::.... rgb uses default-features = false because only the plain RGB8 struct is needed for graph output.
How it works
- Generates test patterns (a dot grid for downscale, a single bright line for upscale).
- Passes them through your resize function.
- Reconstructs the filter kernel from the output pixel values.
- Scores the result against known filters (Box, Triangle, Hermite, Mitchell, Lanczos, ...).
- Returns the best match with correlation, RMS error, and detected support radius.
analyze runs both directions; analyze_downscale (dot pattern, 557->555) and analyze_upscale (line pattern, 15->555) run just one when that's all you need.
Configuration
AnalysisConfig has exactly two fields. It is not #[non_exhaustive], so you can build it with a struct literal or start from AnalysisConfig::default():
pub struct AnalysisConfig {
/// `true` if the resizer works in linear light — i.e. it converts sRGB to
/// linear *before* filtering and back *after*. The analyzer then linearizes
/// the probe output the same way before reconstructing the kernel. Set
/// `false` for a resizer that filters directly on sRGB-encoded samples.
pub srgb: bool,
/// Whether to run edge-handling detection (fills in `edge_mode`).
pub detect_edges: bool,
}
The default is srgb: false, detect_edges: true.
What you get back
analyze() returns Result<AnalysisResult, Error>. Everything below is a plain public field (not an accessor method), so you read it directly. None of these structs is #[non_exhaustive]. Error is WrongDimensions { .. } (your closure returned the wrong output size) or NoData (reconstruction found no usable points).
pub struct AnalysisResult {
/// Filter reconstructed from the dot pattern (557->555 downscale). `None` if
/// reconstruction found no usable points.
pub downscale_curve: Option<FilterCurve>,
/// Filter reconstructed from the line pattern (15->555 upscale). `None` if
/// reconstruction found no usable points.
pub upscale_curve: Option<FilterCurve>,
/// One entry per known filter, sorted best-first by Pearson correlation.
pub scores: Vec<FilterScore>,
/// Detected edge handling. `None` when `AnalysisConfig::detect_edges` is false.
pub edge_mode: Option<EdgeMode>,
}
pub struct FilterScore {
/// Which reference filter this score is against. Implements `Display`
/// (e.g. "Lanczos3", or "Mitchell-Netravali(B=0.333, C=0.333)").
pub filter: KnownFilter,
/// Pearson correlation coefficient, in -1.0..=1.0. 1.0 is a perfect match.
pub correlation: f64,
/// Root-mean-square error between reconstructed and reference weights
/// (filter-weight units; lower is better).
pub rms_error: f64,
/// Largest single absolute weight difference (filter-weight units).
pub max_error: f64,
/// Support radius detected from the reconstructed curve, in source pixels
/// (outermost offset where |weight| > 0.005).
pub detected_support: f64,
/// Support radius the reference filter is defined to have, in source pixels.
pub expected_support: f64,
}
pub struct FilterCurve {
/// (offset, weight) sample points. Offset is in source-pixel units
/// (distance from the filter center); weight is the normalized filter value.
pub points: Vec<(f64, f64)>,
/// Integral of the filter (~1.0 for a normalized filter).
pub area: f64,
/// Scale factor used: dst_width / src_width.
pub scale_factor: f64,
/// True for the dot pattern (scattered points), false for the line pattern
/// (one connected curve).
pub is_scatter: bool,
}
EdgeMode is an enum: Clamp, Reflect, Wrap, Zero, or Unknown (all Display).
Picking the best match
AnalysisResult::best_match() -> Option<&FilterScore> returns the entry with the highest Pearson correlation (the first element of the already-sorted scores), but only when its correlation exceeds 0.99 — otherwise it returns None. Use the filter field (which is Display) to name it:
if let Some(best) = result.best_match() {
println!("Detected: {} (r={:.4})", best.filter, best.correlation);
}
If you want the top candidate regardless of confidence, read scores[0] directly instead.
Graphs
AnalysisResult::render_graph() -> ImgVec<RGB8> draws a 600x300 scope plot of the reconstructed curve(s). render_graph_with_reference(KnownFilter) overlays a named reference filter so you can eyeball the fit:
use resamplescope::KnownFilter;
let graph = result.render_graph_with_reference(KnownFilter::Lanczos3);
// graph is ImgVec<RGB8> — encode it with zenpng, the png crate, etc.
Known filters
KnownFilter: Box, Triangle, Hermite, CatmullRom, Mitchell, BSpline, Lanczos2, Lanczos3, Lanczos4, plus an arbitrary MitchellNetravali { b, c } parameterization. Scoring (KnownFilter::all_named()) compares against the nine fixed filters.
Reference resize and SSIM
The reference module produces mathematically exact resize output for any built-in filter — useful both as a stand-in resizer and as the ground truth in an SSIM comparison:
use resamplescope::{compute_weights, perfect_resize, ssim, KnownFilter};
// Exact separable resize (clamp edges) with a chosen filter:
let resized = perfect_resize(src.as_ref(), 555, 15, KnownFilter::Lanczos3);
// The 1D weight table behind it (per output pixel: source pixels + weights):
let weights = compute_weights(KnownFilter::Lanczos3, 15, 555);
// Block-based SSIM between two equal-size grayscale buffers:
let score = ssim(a, b, width, height); // -> f64, 1.0 == identical
No I/O
This crate has no image-codec dependencies. It works in ImgVec<u8> (grayscale) and ImgVec<RGB8> (graph output); you bring your own PNG/JPEG encoding.
Original work
- Author: Jason Summers
- Website: http://entropymine.com/resamplescope/
- License of original: GPL-3.0-or-later
The test-pattern generation and filter-reconstruction algorithms are ported from the original C source. The reference filter math uses standard mathematical definitions (sinc, Mitchell-Netravali, etc.); the authoritative, optimized implementations of these filters live in imageflow.
License
AGPL-3.0-or-later. See LICENSE. As a derivative of the GPL-3.0-or-later original, distribution stays under copyleft.
Image tech I maintain
| Codecs ¹ | zenjpeg · zenpng · zenwebp · zengif · zenavif · zenjxl · zenbitmaps · heic · zentiff · zenpdf · zensvg · zenjp2 · zenraw · ultrahdr |
| Codec internals | zenjxl-decoder · jxl-encoder · zenrav1e · rav1d-safe · zenavif-parse · zenavif-serialize |
| Compression | zenflate · zenzop · zenzstd |
| Processing | zenresize · zenquant · zenblend · zenfilters · zensally · zentone |
| Pixels & color | zenpixels · zenpixels-convert · linear-srgb · garb |
| Pipeline & framework | zenpipe · zencodec · zencodecs · zenlayout · zennode · zenwasm · zentract |
| Metrics | zensim · fast-ssim2 · butteraugli · zenmetrics · resamplescope-rs |
| Pickers & ML | zenanalyze · zenpredict · zenpicker |
| Products | Imageflow image engine (.NET · Node · Go) · Imageflow Server · ImageResizer (C#) |
¹ pure-Rust, #![forbid(unsafe_code)] codecs, as of 2026
General Rust awesomeness
zenbench · archmage · magetypes · enough · whereat · cargo-copter