Filters: Photoshop's Blur More kernel, Find Edges strength and Polar Coordinates direction (#4652)
Measured against Photoshop 27.11 on a test chart: Blur More uses the [1 2 1; 2 2 2; 1 2 1]/14 kernel, Find Edges the full Sobel magnitude, and Polar Coordinates turns counter-clockwise from 12 o'clock with repeated edges and Photoshop's sampling grid. All four now match within 2 levels. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
cdfec72f71
commit
5f2c23e142
|
|
@ -160,19 +160,30 @@ pub(crate) fn polar(src: &Image, out: Rect, ctx: &Ctx, mode: PolarMode) -> Vec<f
|
|||
let (w, h) = (b.width().max(1) as f32, b.height().max(1) as f32);
|
||||
let (cx, cy) = (b.x0 as f32 + w / 2.0, b.y0 as f32 + h / 2.0);
|
||||
let rmax = w.min(h) / 2.0;
|
||||
remap(src, out, ctx, Edge::Transparent, |x, y| match mode {
|
||||
// Photoshop 27.11: the angle runs counter-clockwise from 12 o'clock, and points past the
|
||||
// source's edge repeat its edge pixels (the corners outside the circle are filled, not empty).
|
||||
// Photoshop evaluates the map at pixel corners and reads the source at the resulting index
|
||||
// (half a pixel from `remap`'s centres): within 2 levels on a whole test chart in both directions.
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| polar_map(x - 0.5, y - 0.5, mode, b, (w, h), (cx, cy), rmax))
|
||||
}
|
||||
|
||||
fn polar_map(x: f32, y: f32, mode: PolarMode, b: Rect, (w, h): (f32, f32), (cx, cy): (f32, f32), rmax: f32) -> (f32, f32) {
|
||||
let (sx, sy) = match mode {
|
||||
PolarMode::RectangularToPolar => {
|
||||
// Output is polar: angle from 12 o'clock clockwise → source x,
|
||||
// radius → source y.
|
||||
// Output is polar: angle from 12 o'clock counter-clockwise → source x, radius → source y.
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let theta = dx.atan2(-dy).rem_euclid(std::f32::consts::TAU);
|
||||
// The 12 o'clock seam itself reads the source's right edge (360°, not 0°).
|
||||
let theta = (-dx).atan2(-dy);
|
||||
let theta = if theta <= 0.0 { theta + std::f32::consts::TAU } else { theta };
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
(b.x0 as f32 + theta / std::f32::consts::TAU * w, b.y0 as f32 + r / rmax * h)
|
||||
}
|
||||
PolarMode::PolarToRectangular => {
|
||||
let theta = (x - b.x0 as f32) / w * std::f32::consts::TAU;
|
||||
// Photoshop's angle is one column ahead of its radius row (fitted exactly).
|
||||
let theta = (x + 1.0 - b.x0 as f32) / w * std::f32::consts::TAU;
|
||||
let r = (y - b.y0 as f32) / h * rmax;
|
||||
(cx + r * theta.sin(), cy - r * theta.cos())
|
||||
(cx - r * theta.sin(), cy - r * theta.cos())
|
||||
}
|
||||
})
|
||||
};
|
||||
(sx + 0.5, sy + 0.5)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,8 +178,9 @@ pub(crate) fn emboss(src: &Image, out: Rect, ctx: &Ctx, angle: f32, height: f32,
|
|||
res
|
||||
}
|
||||
|
||||
/// Find Edges: Sobel magnitude per colour channel, dark edges on white
|
||||
/// (CMYK samples are ink, so there the edge strength is the ink itself).
|
||||
/// Find Edges: Sobel magnitude per colour channel, dark edges on white (CMYK samples are ink, so
|
||||
/// there the edge strength is the ink itself). At full strength, as Photoshop's: a 60-level step
|
||||
/// darkens by 240 (Photoshop 27.11; a quarter of that was 4× too pale).
|
||||
pub(crate) fn find_edges(src: &Image, out: Rect, ctx: &Ctx) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let cc = if ctx.alpha { n - 1 } else { n };
|
||||
|
|
@ -192,7 +193,7 @@ pub(crate) fn find_edges(src: &Image, out: Rect, ctx: &Ctx) -> Vec<f32> {
|
|||
let g = |dx: i32, dy: i32| src.get(x + dx, y + dy, c);
|
||||
let gx = (g(1, -1) + 2.0 * g(1, 0) + g(1, 1)) - (g(-1, -1) + 2.0 * g(-1, 0) + g(-1, 1));
|
||||
let gy = (g(-1, 1) + 2.0 * g(0, 1) + g(1, 1)) - (g(-1, -1) + 2.0 * g(0, -1) + g(1, -1));
|
||||
let e = ((gx * gx + gy * gy).sqrt() / 4.0).clamp(0.0, 1.0);
|
||||
let e = (gx * gx + gy * gy).sqrt().clamp(0.0, 1.0);
|
||||
*v = if sub { e } else { 1.0 - e };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -488,6 +488,21 @@ fn layer_filters_repeat_the_canvas_edge() {
|
|||
}
|
||||
}
|
||||
|
||||
/// Photoshop 27.11's Polar Coordinates turn counter-clockwise from 12 o'clock: just left of the
|
||||
/// top centre, Rectangular to Polar samples the source's left edge (clockwise would take its right).
|
||||
#[test]
|
||||
fn polar_coordinates_turn_counter_clockwise() {
|
||||
let r = Rect::new(0, 0, 64, 64);
|
||||
let mut s = flat(SampleType::U8, r, [0.0, 0.0, 1.0, 1.0]);
|
||||
s.fill_rect(Rect::new(0, 0, 32, 64), &[1.0, 0.0, 0.0, 1.0]);
|
||||
let p = FilterParams::PolarCoordinates { mode: PolarMode::RectangularToPolar };
|
||||
let out = apply(&s, &p, r, r, None);
|
||||
let left_of_top = out.rgba(28, 12);
|
||||
assert!(left_of_top[0] > 0.9 && left_of_top[2] < 0.1, "{left_of_top:?}");
|
||||
// The corners outside the circle repeat the source's edge instead of going transparent.
|
||||
assert!(out.rgba(0, 0)[3] > 0.99, "{:?}", out.rgba(0, 0));
|
||||
}
|
||||
|
||||
/// `mix_selection` before #4533, one selection lookup per pixel: the oracle for the per-tile read.
|
||||
fn mix_selection_reference(data: &mut [f32], t: Rect, sel: &Surface, orig: &crate::image::Image, alpha: bool) {
|
||||
let w = t.width() as usize;
|
||||
|
|
|
|||
|
|
@ -52,7 +52,22 @@ fn preserve(p: &Value) -> Preserve {
|
|||
fn preset(id: &str) -> Option<(&'static str, FilterParams)> {
|
||||
Some(match id {
|
||||
"filter.blur.blur" => ("Blur", FilterParams::GaussianBlur { radius: 0.6 }),
|
||||
"filter.blur.blurMore" => ("Blur More", FilterParams::GaussianBlur { radius: 1.4 }),
|
||||
// Photoshop 27.11's Blur More is the 3×3 kernel [1 2 1; 2 2 2; 1 2 1] / 14 (its response
|
||||
// to a single white pixel, exactly).
|
||||
"filter.blur.blurMore" => (
|
||||
"Blur More",
|
||||
FilterParams::Custom {
|
||||
kernel: vec![
|
||||
0.0, 0.0, 0.0, 0.0, 0.0, //
|
||||
0.0, 1.0, 2.0, 1.0, 0.0, //
|
||||
0.0, 2.0, 2.0, 2.0, 0.0, //
|
||||
0.0, 1.0, 2.0, 1.0, 0.0, //
|
||||
0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
],
|
||||
scale: 14.0,
|
||||
offset: 0.0,
|
||||
},
|
||||
),
|
||||
"filter.sharpen.sharpen" => ("Sharpen", FilterParams::UnsharpMask { amount: 60.0, radius: 0.5, threshold: 0.0 }),
|
||||
"filter.sharpen.sharpenMore" => ("Sharpen More", FilterParams::UnsharpMask { amount: 150.0, radius: 0.6, threshold: 0.0 }),
|
||||
"filter.sharpen.sharpenEdges" => ("Sharpen Edges", FilterParams::UnsharpMask { amount: 100.0, radius: 0.8, threshold: 6.0 }),
|
||||
|
|
|
|||
Loading…
Reference in New Issue