Skip to content

Bilateral speedup - #798

Open
CattleProdigy wants to merge 2 commits into
image-rs:mainfrom
CattleProdigy:bilateral-speedup-pr
Open

Bilateral speedup#798
CattleProdigy wants to merge 2 commits into
image-rs:mainfrom
CattleProdigy:bilateral-speedup-pr

Conversation

@CattleProdigy

Copy link
Copy Markdown
Contributor

Even after the last round of performance improvements to the bilateral filter there was still a pretty big performance drop starting after 0.25. It turned out to mostly because of confusing the window-radius vs window-size argument change. Nonetheless, I kept making performance improvements.

This PR has two more improvements:

  1. Introduction of a LUT-based color-distance implementation for u8-subpixel pixel types. This LUT implementation has twice the memory footprint, but is faster since it avoids an absolute value.
  2. Separate code paths for pixels in the image boundary which requires clamping and pixel in the middle which don't. Clamping code showed up as a fairly large runtime component when I perfed/flamegraphed this.

Timing

Dynamic Color-Distance

Baseline

gray: 70ms
rgb: 116ms

With Clamping Rework

gray: 70ms -> 51ms
rgb: 116 -> 93ms

LUT-based Color Distance

New LUT

gray: 38ms
rgb: 79ms

New LUT + Clamping Rework

gray: 38ms -> 17ms
rgb: 79ms -> 59ms

0.25 LUT-based gray only

gray: 70ms

@CattleProdigy CattleProdigy changed the title Bilateral speedup pr Bilateral speedup Jul 13, 2026
Comment thread src/filter/bilateral.rs
for i in 0..num_channels {
out_channels[i] = (channel_sums[i] / weight_sum).as_();
// Bottom edge
for y in (height - radius)..height {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this overlaps with the previous loop of y in 0..radius, for example:

radius = 7
height = 10

0..radius <=> 0..7
(height - radius)..height <=> (10 - 7)..10 <=> 3..10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will do

debug_assert!(radius <= height)
let interior_end = (height - radius).max(radius);

for y in 0..radius {}

for y in radius..interior_end {}

for y in interior_end..height {}

Comment thread src/filter/bilateral.rs
/// (used for the interior). Because `CLAMP` is a const generic, each variant
/// monomorphizes to straight-line code with the unused branch eliminated.
#[inline(always)]
fn filter<const CLAMP: bool>(&self, x: u32, y: u32) -> P {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be unsafe

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and needs testing

Comment thread src/filter/bilateral.rs
}

impl LutGaussianEuclideanColorDistance {
/// Creates a new [`GaussianEuclideanColorDistance`] using a given sigma value, which

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LutGaussianEuclideanColorDistance]

Comment thread src/filter/bilateral.rs
pub fn new(sigma: f32) -> Self {
assert!(
sigma > 0.0,
"GaussianEuclideanColorDistance sigma must be positive"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LutGaussianEuclideanColorDistance

Comment thread src/filter/bilateral.rs
/// a look up table. This gives substantial speed up for pixel's with a u8 subpixel.
///
/// This implements [`ColorDistance`].
pub struct LutGaussianEuclideanColorDistance {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs testing

Comment thread src/filter/bilateral.rs
channel_sums[i] += weight * f32::from(*c);
}
// Right edge
for x in (width - radius)..width {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overlaps similar to comment below

Comment thread src/filter/bilateral.rs
let num_channels = P::CHANNEL_COUNT as usize;
let out_channels = out_pixel.channels_mut();
for i in 0..num_channels {
out_channels[i] = (channel_sums[i] / weight_sum).as_();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably faster with zip because out_channels is a slice, plus will not go out of bounds

Comment thread src/filter/bilateral.rs
sigma > 0.0,
"GaussianEuclideanColorDistance sigma must be positive"
);
let mut lut = Vec::with_capacity(512);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe lets allocate [f32; 512] on the stack?

Comment thread src/filter/bilateral.rs

let radius = i16::from(radius);
let (width, height) = image.dimensions();
let radius = (radius as u32).min(width).min(height);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should document that radius is clamped to be in bounds

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants