M13.9: gpu Shadow/Highlight, CC Color Neutralizer, Invert HLS / YIQ
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7a96fab302
commit
3bf7c4bcd2
|
|
@ -453,17 +453,29 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// ---------------------------------------------------------------------------------------------
|
||||
// Shadow/Highlight
|
||||
|
||||
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Shadow/Highlight's shadow and highlight amounts for `img` (Auto Amounts measures them).
|
||||
pub fn shadow_highlight_amounts(ctx: &EffectCtx, img: &Image) -> (f32, f32) {
|
||||
let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0);
|
||||
let luma = Plane::luma(&b.img);
|
||||
let src_hist = histograms(&b.img);
|
||||
if ctx.params.b("autoAmounts") {
|
||||
// Temporal Smoothing / Scene Detect average the analysis over neighbouring frames.
|
||||
let mean =
|
||||
if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, &b.img)[3]) as f32 } else { hist_mean(&src_hist[3]) as f32 };
|
||||
let mean = if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, img)[3]) } else { hist_mean(&histograms(img)[3]) } as f32;
|
||||
s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1;
|
||||
h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8;
|
||||
}
|
||||
(s_amt, h_amt)
|
||||
}
|
||||
|
||||
/// The luminance values clipping `black` / `white` fractions of `img`'s alpha-weighted
|
||||
/// luminance histogram (Shadow/Highlight's Black / White Clip).
|
||||
pub fn luma_clip_points(img: &Image, black: f64, white: f64) -> (f32, f32) {
|
||||
clip_points(&histograms(img)[3], black, white)
|
||||
}
|
||||
|
||||
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (s_amt, h_amt) = shadow_highlight_amounts(ctx, &b.img);
|
||||
let luma = Plane::luma(&b.img);
|
||||
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||
let clip0 = (bc > 0.0 || wc > 0.0).then(|| luma_clip_points(&b.img, bc, wc));
|
||||
let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01);
|
||||
let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
||||
|
|
@ -495,10 +507,8 @@ fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels)
|
||||
// are mapped back onto the original's, so lifting shadows or recovering highlights doesn't
|
||||
// flatten the ends of the tonal range.
|
||||
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||
if bc > 0.0 || wc > 0.0 {
|
||||
let (lo0, hi0) = clip_points(&src_hist[3], bc, wc);
|
||||
let (lo1, hi1) = clip_points(&histograms(&b.img)[3], bc, wc);
|
||||
if let Some((lo0, hi0)) = clip0 {
|
||||
let (lo1, hi1) = luma_clip_points(&b.img, bc, wc);
|
||||
if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 {
|
||||
let k = (hi0 - lo0) / (hi1 - lo1);
|
||||
map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a));
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ pub use color_fx::{
|
|||
HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity,
|
||||
levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip,
|
||||
};
|
||||
pub use color2::{Curve, auto_correct_settings, equalize_tables};
|
||||
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::parse_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
|
|
@ -850,6 +850,8 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.color.autocontrast",
|
||||
"ec.color.autocolor",
|
||||
"ec.color.equalize",
|
||||
"ec.color.shadowhighlight",
|
||||
"ec.color.cccolorneutralizer",
|
||||
// effectcraft-gpu fx_stylize (stylize, distort)
|
||||
//
|
||||
// effectcraft-gpu fx_noise (noise, blur, time)
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
"ec.color.curves" => curves(e, ctx, b),
|
||||
"ec.color.huesaturation" if !effectcraft_effects::huesat_ranges_identity(ctx) => crate::fx_tone::hue_saturation(e, ctx, b),
|
||||
"ec.color.levels" if !effectcraft_effects::levels_channels_identity(ctx) => crate::fx_tone::levels(e, ctx, b),
|
||||
"ec.channel.invert" if (4..=11).contains(&ctx.params.e("channel")) => crate::fx_tone::invert(e, ctx, b),
|
||||
_ if id.starts_with("ec.control.") => Some(b),
|
||||
_ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b),
|
||||
_ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use effectcraft_color::rgb_to_hsl;
|
|||
use effectcraft_effects::EffectCtx;
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::GBuf;
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_tone.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxt_point"];
|
||||
|
|
@ -37,11 +37,63 @@ pub(crate) const IDS: &[&str] = &[
|
|||
"ec.color.autocontrast",
|
||||
"ec.color.autocolor",
|
||||
"ec.color.equalize",
|
||||
"ec.color.shadowhighlight",
|
||||
"ec.color.cccolorneutralizer",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
point(e, id, ctx, b)
|
||||
match id {
|
||||
"ec.color.shadowhighlight" => shadow_highlight(e, ctx, b),
|
||||
_ => point(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Invert in the HLS and YIQ channels (the base `pointwise` kernel does RGB and Alpha).
|
||||
pub(crate) fn invert(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let mut p = op(18);
|
||||
p.u[0][1] = ctx.params.e("channel");
|
||||
p.f[0][0] = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
run(e, &p, b, None)
|
||||
}
|
||||
|
||||
/// Shadow/Highlight: the amounts (Auto Amounts) and the Black / White Clip ranges are measured
|
||||
/// on the CPU; the luminance bases are blurred and the image adjusted on the GPU.
|
||||
fn shadow_highlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let src = e.download(&b.img)?;
|
||||
let (s_amt, h_amt) = effectcraft_effects::shadow_highlight_amounts(ctx, &src);
|
||||
let (bc, wc) = (pr.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, pr.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||
let clip0 = (bc > 0.0 || wc > 0.0).then(|| effectcraft_effects::luma_clip_points(&src, bc, wc));
|
||||
let s_r = pr.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
||||
let h_r = pr.f("moreOptions/highlightRadius").max(0.0) * b.scale / 2.0;
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
let luma = e.image(w, h);
|
||||
e.pixels("fxt_point", &op(19), &b.img, None, &luma, None);
|
||||
let base_s = gaussian_blur(e, &luma, s_r, s_r, true);
|
||||
let base_h = if (h_r - s_r).abs() < 1e-9 { base_s.clone() } else { gaussian_blur(e, &luma, h_r, h_r, true) };
|
||||
let bases = e.image(w, h);
|
||||
e.pixels("fxt_point", &op(21), &base_s, Some(&base_h), &bases, None);
|
||||
let mut p = op(20);
|
||||
p.f[0] =
|
||||
[s_amt, h_amt, (pr.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01), (pr.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01)];
|
||||
p.f[1] = [
|
||||
pr.f("moreOptions/colorCorrection") as f32 / 100.0,
|
||||
pr.f("moreOptions/midtoneContrast") as f32 / 100.0,
|
||||
(pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32,
|
||||
0.0,
|
||||
];
|
||||
let out = e.image(w, h);
|
||||
e.pixels("fxt_point", &p, &b.img, Some(&bases), &out, None);
|
||||
let Some((lo0, hi0)) = clip0 else { return Some(GBuf { img: out, ..b }) };
|
||||
let adjusted = e.download(&out)?;
|
||||
let (lo1, hi1) = effectcraft_effects::luma_clip_points(&adjusted, bc, wc);
|
||||
if (lo0, hi0) == (lo1, hi1) || hi1 - lo1 <= 1e-3 {
|
||||
return Some(GBuf { img: out, ..b });
|
||||
}
|
||||
let mut p = op(22);
|
||||
p.f[0] = [lo0, lo1, (hi0 - lo0) / (hi1 - lo1), 0.0];
|
||||
run(e, &p, GBuf { img: out, ..b }, None)
|
||||
}
|
||||
|
||||
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||
|
|
@ -267,6 +319,16 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
|||
p.f[0] = [level, level * (1.0 - comp), 0.0, 0.0];
|
||||
p.f[1] = rgb(pr.color("gamutWarningColor"));
|
||||
}
|
||||
"ec.color.cccolorneutralizer" => {
|
||||
let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")];
|
||||
for (i, (u, bal)) in pairs.iter().enumerate() {
|
||||
let (u, v) = (pr.color(u), pr.color(bal));
|
||||
p.f[i] = [v[0] - u[0], v[1] - u[1], v[2] - u[2], 0.0];
|
||||
}
|
||||
p.u[0][0] = 23;
|
||||
p.f[3] = [f("pinning") / 100.0, f("blendWOriginal") / 100.0, f("contrast") / 100.0, 0.0];
|
||||
p.f[4] = [f("darks") / 100.0, f("brights") / 100.0, 0.0, 0.0];
|
||||
}
|
||||
"ec.color.autolevels" | "ec.color.autocontrast" | "ec.color.autocolor" | "ec.color.equalize" => {
|
||||
// The histograms are measured on the CPU.
|
||||
let img = e.download(&b.img)?;
|
||||
|
|
|
|||
|
|
@ -144,7 +144,9 @@ fn fxt_eq_look(table: u32, n: u32, v: f32) -> f32 {
|
|||
// 5 Leave Color 6 Change Color 7 Broadcast Colors 8 Color Balance (HLS) 9 CC Toner
|
||||
// 10 CC Color Offset 11 CC Kernel 12 PS Arbitrary Map 13 Video Limiter
|
||||
// 14 Hue/Saturation with colour ranges 15 Levels with channel controls
|
||||
// 16 Auto Levels / Contrast / Color apply 17 Equalize apply
|
||||
// 16 Auto Levels / Contrast / Color apply 17 Equalize apply 18 Invert (HLS / YIQ channels)
|
||||
// 19 luminance plane 20 Shadow/Highlight (aux = (shadow base, highlight base))
|
||||
// 21 pack src.x, aux.x 22 Shadow/Highlight clip remap 23 CC Color Neutralizer
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
|
|
@ -586,7 +588,118 @@ fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||
o = vec4<f32>(r * na, na);
|
||||
}
|
||||
case 18u: {
|
||||
if (a <= 0.0) {
|
||||
break;
|
||||
}
|
||||
let ch = u0.y;
|
||||
var r: vec3<f32>;
|
||||
if (ch <= 7u) {
|
||||
var hsl = rgb_to_hsl(c);
|
||||
if (ch == 4u || ch == 5u) {
|
||||
hsl.x = 1.0 - hsl.x;
|
||||
}
|
||||
if (ch == 4u || ch == 6u) {
|
||||
hsl.z = 1.0 - hsl.z;
|
||||
}
|
||||
if (ch == 4u || ch == 7u) {
|
||||
hsl.y = 1.0 - hsl.y;
|
||||
}
|
||||
r = hsl_to_rgb(hsl.x, hsl.y, hsl.z);
|
||||
} else {
|
||||
var q = vec3<f32>(
|
||||
0.299 * c.x + 0.587 * c.y + 0.114 * c.z,
|
||||
0.596 * c.x - 0.274 * c.y - 0.322 * c.z,
|
||||
0.211 * c.x - 0.523 * c.y + 0.312 * c.z,
|
||||
);
|
||||
if (ch == 8u || ch == 9u) {
|
||||
q.x = 1.0 - q.x;
|
||||
}
|
||||
if (ch == 8u || ch == 10u) {
|
||||
q.y = -q.y;
|
||||
}
|
||||
if (ch == 8u || ch == 11u) {
|
||||
q.z = -q.z;
|
||||
}
|
||||
r = max(
|
||||
vec3<f32>(q.x + 0.956 * q.y + 0.621 * q.z, q.x - 0.272 * q.y - 0.647 * q.z, q.x - 1.106 * q.y + 1.703 * q.z),
|
||||
vec3<f32>(0.0),
|
||||
);
|
||||
}
|
||||
o = vec4<f32>((c + (r - c) * f0.x) * a, a);
|
||||
}
|
||||
case 19u: {
|
||||
o = vec4<f32>(luminance(c));
|
||||
}
|
||||
case 20u: {
|
||||
if (a <= 0.0) {
|
||||
break;
|
||||
}
|
||||
let bases = textureLoad(aux, p, 0);
|
||||
let s_amt = f0.x;
|
||||
let h_amt = f0.y;
|
||||
let ws0 = clamp(1.0 - bases.x / f0.z, 0.0, 1.0);
|
||||
let wh0 = clamp((bases.y - (1.0 - f0.w)) / f0.w, 0.0, 1.0);
|
||||
let ws = ws0 * ws0;
|
||||
let wh = wh0 * wh0;
|
||||
let gain = (1.0 + s_amt * ws * 2.0) * (1.0 - h_amt * wh * 0.5);
|
||||
let l = luminance(c);
|
||||
let nl = l * gain;
|
||||
let sat = pow(max(gain, 1e-3), P.f[1].x);
|
||||
var r = nl + (c - l) * sat;
|
||||
let mc = P.f[1].y;
|
||||
if (mc != 0.0) {
|
||||
let shift = (0.5 + (nl - 0.5) * (1.0 + mc)) - nl;
|
||||
r = r + shift;
|
||||
}
|
||||
r = fxt_mix(max(r, vec3<f32>(0.0)), c, P.f[1].z);
|
||||
o = vec4<f32>(r * a, a);
|
||||
}
|
||||
case 21u: {
|
||||
o = vec4<f32>(px.x, textureLoad(aux, p, 0).x, 0.0, 0.0);
|
||||
}
|
||||
case 22u: {
|
||||
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||
break;
|
||||
}
|
||||
let r = max(f0.x + (c - f0.y) * f0.z, vec3<f32>(0.0));
|
||||
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||
o = vec4<f32>(r * na, na);
|
||||
}
|
||||
case 23u: {
|
||||
if (a <= 0.0) {
|
||||
break;
|
||||
}
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
let ws = 1.0 - fxt_smoothstep(0.0, 0.5, l);
|
||||
let wh = fxt_smoothstep(0.5, 1.0, l);
|
||||
let wm = 1.0 - ws - wh;
|
||||
let pin = P.f[3].x;
|
||||
let keep = 1.0 - pin * clamp(1.0 - 4.0 * l * (1.0 - l), 0.0, 1.0);
|
||||
let contrast = P.f[3].z;
|
||||
let darks = P.f[4].x;
|
||||
let brights = P.f[4].y;
|
||||
var r: vec3<f32>;
|
||||
for (var k = 0; k < 3; k++) {
|
||||
let d = P.f[0][k] * ws + P.f[1][k] * wm + P.f[2][k] * wh;
|
||||
var v = c[k] + d * keep;
|
||||
v = 0.5 + (v - 0.5) * (1.0 + contrast);
|
||||
v += darks * 0.25 * (1.0 - l) * (1.0 - l) + brights * 0.25 * l * l;
|
||||
r[k] = max(v, 0.0);
|
||||
}
|
||||
r = fxt_mix(r, c, P.f[3].y);
|
||||
o = vec4<f32>(r * a, a);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
|
||||
// util::smoothstep.
|
||||
fn fxt_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
|
||||
if (abs(e1 - e0) < 1e-9) {
|
||||
return select(1.0, 0.0, x < e0);
|
||||
}
|
||||
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
|
||||
return t * t * (3.0 - 2.0 * t);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,3 +115,30 @@ fn automatic_corrections() {
|
|||
effect_case("ec.color.equalize", &[("style", e(1)), ("amount", n(70.0))]);
|
||||
effect_case("ec.color.equalize", &[("style", e(2))]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shadow_highlight_neutralizer_invert() {
|
||||
effect_case("ec.color.shadowhighlight", &[]);
|
||||
effect_case(
|
||||
"ec.color.shadowhighlight",
|
||||
&[
|
||||
("autoAmounts", off()),
|
||||
("shadowAmount", n(70.0)),
|
||||
("highlightAmount", n(40.0)),
|
||||
("moreOptions/shadowRadius", n(8.0)),
|
||||
("moreOptions/highlightRadius", n(15.0)),
|
||||
("moreOptions/midtoneContrast", n(30.0)),
|
||||
("moreOptions/blackClip", n(2.0)),
|
||||
("blend", n(20.0)),
|
||||
],
|
||||
);
|
||||
effect_case("ec.color.shadowhighlight", &[("moreOptions/blackClip", n(0.0)), ("moreOptions/whiteClip", n(0.0)), ("moreOptions/colorCorrection", n(-40.0))]);
|
||||
effect_case(
|
||||
"ec.color.cccolorneutralizer",
|
||||
&[("shadowsBalance", c(0.1, 0.0, 0.05)), ("midtonesUnbalance", c(0.6, 0.5, 0.4)), ("pinning", n(40.0)), ("contrast", n(20.0)), ("darks", n(-30.0))],
|
||||
);
|
||||
effect_case("ec.color.cccolorneutralizer", &[("highlightsBalance", c(0.9, 1.0, 0.8)), ("brights", n(40.0)), ("blendWOriginal", n(25.0))]);
|
||||
for ch in 4..=11 {
|
||||
effect_case("ec.channel.invert", &[("channel", e(ch)), ("blend", n(20.0))]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue