mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-24 09:57:08 -06:00
Merge pr/1511
# Conflicts: # contributors/people.toml
This commit is contained in:
@@ -39,3 +39,8 @@ real_name = "Arthur Vinicius"
|
||||
|
||||
[people.SckraT]
|
||||
sync_github_name = true
|
||||
|
||||
[people.GamerWil26]
|
||||
real_name = "Willy Foo"
|
||||
display_name = "Willy Foo"
|
||||
sync_github_name = false
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
//! Deterministic 24 MP blur/resize benchmark. Run with --release; no private photos needed.
|
||||
use photocraft_algo::{
|
||||
FilterParams, apply_in,
|
||||
resample::{Resample, resize_surface_in_canvas},
|
||||
};
|
||||
use photocraft_color::{ColorMode, PixelFormat, SampleType};
|
||||
use photocraft_geom::Rect;
|
||||
use photocraft_raster::Surface;
|
||||
use std::time::Instant;
|
||||
fn main() {
|
||||
let bounds = Rect::new(0, 0, 6000, 4000);
|
||||
let mut src = Surface::new(PixelFormat::new(ColorMode::Rgb, SampleType::U8, true));
|
||||
for y in (0..4000).step_by(128) {
|
||||
let r = Rect::new(0, y, 6000, (y + 128).min(4000));
|
||||
let mut data = Vec::new();
|
||||
for yy in r.y0..r.y1 {
|
||||
for x in r.x0..r.x1 {
|
||||
data.extend_from_slice(&[(x % 251) as f32 / 250.0, (yy % 241) as f32 / 240.0, ((x + yy) % 239) as f32 / 238.0, 1.0]);
|
||||
}
|
||||
}
|
||||
src.write_region(r, &data);
|
||||
}
|
||||
for op in ["blur3", "bicubic1791"] {
|
||||
for rep in 0..4 {
|
||||
let t = Instant::now();
|
||||
let out = if op == "blur3" {
|
||||
apply_in(&src, &FilterParams::GaussianBlur { radius: 3.0 }, bounds, bounds, None, bounds)
|
||||
} else {
|
||||
resize_surface_in_canvas(&src, 1791.0 / 6000.0, 1194.0 / 4000.0, Resample::Bicubic, bounds)
|
||||
};
|
||||
let elapsed = t.elapsed().as_secs_f64() * 1000.0;
|
||||
let sample = out.read_region(Rect::new(100, 100, 140, 130));
|
||||
let checksum = sample.iter().fold(0u64, |s, v| s.wrapping_mul(31).wrapping_add(v.to_bits() as u64));
|
||||
println!("{op} rep={rep} ms={elapsed:.3} checksum={checksum}");
|
||||
std::hint::black_box(out);
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
-6
@@ -55,12 +55,21 @@ pub(crate) fn conv_sep(src: &Image, out: Rect, kx: &[f32], ky: &[f32], alpha: bo
|
||||
for ty in 0..th {
|
||||
let row = &p[ty * ww * n..(ty + 1) * ww * n];
|
||||
let dst = &mut tmp[ty * ow * n..(ty + 1) * ow * n];
|
||||
for ox in 0..ow {
|
||||
let d = &mut dst[ox * n..(ox + 1) * n];
|
||||
for (i, kv) in kx.iter().enumerate() {
|
||||
let s = &row[(ox + i) * n..(ox + i + 1) * n];
|
||||
for c in 0..n {
|
||||
d[c] += s[c] * kv;
|
||||
match n {
|
||||
1 => conv_row::<1>(row, dst, kx),
|
||||
2 => conv_row::<2>(row, dst, kx),
|
||||
3 => conv_row::<3>(row, dst, kx),
|
||||
4 => conv_row::<4>(row, dst, kx),
|
||||
5 => conv_row::<5>(row, dst, kx),
|
||||
_ => {
|
||||
for ox in 0..ow {
|
||||
let d = &mut dst[ox * n..(ox + 1) * n];
|
||||
for (i, kv) in kx.iter().enumerate() {
|
||||
let s = &row[(ox + i) * n..(ox + i + 1) * n];
|
||||
for c in 0..n {
|
||||
d[c] += s[c] * kv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,6 +89,21 @@ pub(crate) fn conv_sep(src: &Image, out: Rect, kx: &[f32], ky: &[f32], alpha: bo
|
||||
res
|
||||
}
|
||||
|
||||
// Fixed channel counts let LLVM keep one pixel's accumulators in registers and vectorize
|
||||
// channels. The tap order is unchanged; unusual multichannel images keep the generic path.
|
||||
fn conv_row<const N: usize>(src: &[f32], dst: &mut [f32], kernel: &[f32]) {
|
||||
let pixels = src.as_chunks::<N>().0;
|
||||
for (x, out) in dst.as_chunks_mut::<N>().0.iter_mut().enumerate() {
|
||||
let mut acc = [0.0f32; N];
|
||||
for (pixel, weight) in pixels.get(x..).unwrap_or(&[]).iter().zip(kernel) {
|
||||
for c in 0..N {
|
||||
acc[c] += pixel[c] * weight;
|
||||
}
|
||||
}
|
||||
*out = acc;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn gaussian(src: &Image, out: Rect, ctx: &Ctx, radius: f32) -> Vec<f32> {
|
||||
// Exact kernel for small radii; beyond that three box passes approximate the Gaussian with
|
||||
// cost independent of the radius (Kovesi, "Fast almost-Gaussian filtering", 2010).
|
||||
@@ -803,6 +827,34 @@ mod tests {
|
||||
assert!(coarse < expected * 0.1, "former 64-interval sampling unexpectedly resolved the patch: {coarse} vs {expected}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_channel_convolution_matches_scalar_tap_order() {
|
||||
fn check<const N: usize>() {
|
||||
for width in [1, 7, 256] {
|
||||
for radius in [0.0, 0.5, 3.0, 4.0] {
|
||||
let k = gaussian_kernel(radius);
|
||||
let src: Vec<f32> = (0..(width + k.len() - 1) * N).map(|i| ((i * 73) % 251) as f32 / 250.0).collect();
|
||||
let mut expected = vec![0.0f32; width * N];
|
||||
for x in 0..width {
|
||||
for (i, w) in k.iter().enumerate() {
|
||||
for c in 0..N {
|
||||
expected[x * N + c] += src[(x + i) * N + c] * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut actual = vec![0.0f32; width * N];
|
||||
conv_row::<N>(&src, &mut actual, &k);
|
||||
assert_eq!(actual, expected, "channels={N} width={width} radius={radius}");
|
||||
}
|
||||
}
|
||||
}
|
||||
check::<1>();
|
||||
check::<2>();
|
||||
check::<3>();
|
||||
check::<4>();
|
||||
check::<5>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn box_gaussian_matches_exact_kernel() {
|
||||
// A hard vertical edge (premultiplied RGBA-ish, 2 channels) blurred with σ = 12.
|
||||
|
||||
+67
-14
@@ -148,6 +148,41 @@ pub fn resize_surface_in_canvas(s: &Surface, sx: f64, sy: f64, filter: Resample,
|
||||
/// Samples one source read may hold (64 MB of `f32`); larger windows are read in row chunks.
|
||||
const READ_BUDGET: usize = 1 << 24;
|
||||
|
||||
// Preserve tap order and edge weights while exposing the common channel counts to LLVM.
|
||||
fn horizontal_fixed<const N: usize>(row: &[f32], out: &mut [f32], taps: &[Taps], x_lo: i32, def: &[f32]) {
|
||||
let pixels = row.as_chunks::<N>().0;
|
||||
for (t, d) in taps.iter().zip(out.as_chunks_mut::<N>().0) {
|
||||
let mut acc = [0.0f32; N];
|
||||
for (k, w) in t.weights.iter().enumerate() {
|
||||
if let Some(sp) = pixels.get((t.start + k as i32 - x_lo) as usize) {
|
||||
for c in 0..N {
|
||||
acc[c] += sp[c] * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (c, v) in acc.iter_mut().enumerate() {
|
||||
*v += def.get(c).copied().unwrap_or(0.0) * t.outside;
|
||||
}
|
||||
*d = acc;
|
||||
}
|
||||
}
|
||||
|
||||
fn horizontal_dynamic(row: &[f32], out: &mut [f32], taps: &[Taps], x_lo: i32, def: &[f32], n: usize) {
|
||||
for (t, d) in taps.iter().zip(out.chunks_exact_mut(n)) {
|
||||
for (k, w) in t.weights.iter().enumerate() {
|
||||
let sx_ = (t.start + k as i32 - x_lo) as usize;
|
||||
if let Some(sp) = row.get(sx_ * n..(sx_ + 1) * n) {
|
||||
for c in 0..n {
|
||||
d[c] += sp[c] * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in 0..n {
|
||||
d[c] += def[c] * t.outside;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resize(s: &Surface, sx: f64, sy: f64, filter: Resample, edge: Option<Rect>) -> Surface {
|
||||
resize_with_budget(s, sx, sy, filter, edge, READ_BUDGET)
|
||||
}
|
||||
@@ -174,20 +209,13 @@ fn resize_with_budget(s: &Surface, sx: f64, sy: f64, filter: Resample, edge: Opt
|
||||
const BAND: usize = 64;
|
||||
let bands: Vec<(usize, usize)> = (0..vy.len()).step_by(BAND).map(|b| (b, (b + BAND).min(vy.len()))).collect();
|
||||
// Horizontal pass of one source row (`row` spans `x_lo..x_hi`) into `dw` samples.
|
||||
let horizontal = |row: &[f32], out: &mut [f32]| {
|
||||
for (t, d) in hx.iter().zip(out.chunks_exact_mut(n)) {
|
||||
for (k, w) in t.weights.iter().enumerate() {
|
||||
let sx_ = (t.start + k as i32 - x_lo) as usize;
|
||||
if let Some(sp) = row.get(sx_ * n..(sx_ + 1) * n) {
|
||||
for c in 0..n {
|
||||
d[c] += sp[c] * w;
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in 0..n {
|
||||
d[c] += def[c] * t.outside;
|
||||
}
|
||||
}
|
||||
let horizontal = |row: &[f32], out: &mut [f32]| match n {
|
||||
1 => horizontal_fixed::<1>(row, out, &hx, x_lo, &def),
|
||||
2 => horizontal_fixed::<2>(row, out, &hx, x_lo, &def),
|
||||
3 => horizontal_fixed::<3>(row, out, &hx, x_lo, &def),
|
||||
4 => horizontal_fixed::<4>(row, out, &hx, x_lo, &def),
|
||||
5 => horizontal_fixed::<5>(row, out, &hx, x_lo, &def),
|
||||
_ => horizontal_dynamic(row, out, &hx, x_lo, &def, n),
|
||||
};
|
||||
// A source row of nothing but the default pixel, after the horizontal pass.
|
||||
let def_row = {
|
||||
@@ -313,6 +341,31 @@ mod tests {
|
||||
s
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_channel_rows_match_dynamic_resampling() {
|
||||
fn check<const N: usize>() {
|
||||
for scale in [0.12, 0.5, 1.0, 2.3] {
|
||||
for filter in [Resample::Nearest, Resample::Bilinear, Resample::Bicubic, Resample::Lanczos] {
|
||||
for clamp in [None, Some((-3, 31))] {
|
||||
let ts = taps(-5, 41, scale, filter, clamp, (-3, 31));
|
||||
let row: Vec<f32> = (0..35 * N).map(|i| ((i * 73) % 251) as f32 / 250.0).collect();
|
||||
let def: Vec<f32> = (0..N).map(|i| i as f32 / 7.0).collect();
|
||||
let mut expected = vec![0.0; ts.len() * N];
|
||||
let mut actual = expected.clone();
|
||||
horizontal_dynamic(&row, &mut expected, &ts, -3, &def, N);
|
||||
horizontal_fixed::<N>(&row, &mut actual, &ts, -3, &def);
|
||||
assert_eq!(actual, expected, "N={N} scale={scale} filter={filter:?} clamp={clamp:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
check::<1>();
|
||||
check::<2>();
|
||||
check::<3>();
|
||||
check::<4>();
|
||||
check::<5>();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn identity_scale_is_lossless() {
|
||||
for f in [Resample::Nearest, Resample::Bilinear, Resample::Bicubic, Resample::Lanczos] {
|
||||
|
||||
Reference in New Issue
Block a user