M13.9: gpu Color Stabilizer apply pass
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6cbb6bfb93
commit
6e556690ad
|
|
@ -90,6 +90,8 @@ pub use transition::place_layer;
|
|||
pub use noise::remove_grain_level;
|
||||
pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use time_fx::{posterized_time, time_frames};
|
||||
// effectcraft-gpu fx_tone.
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
|
|
@ -856,6 +858,7 @@ pub const GPU_EFFECTS: &[&str] = &[
|
|||
"ec.color.equalize",
|
||||
"ec.color.shadowhighlight",
|
||||
"ec.color.cccolorneutralizer",
|
||||
"ec.color.colorstabilizer",
|
||||
// effectcraft-gpu fx_stylize (stylize, distort)
|
||||
//
|
||||
// effectcraft-gpu fx_noise (noise, blur, time)
|
||||
|
|
|
|||
|
|
@ -905,6 +905,46 @@ fn curve3(xs: [f64; 3], ys: [f64; 3], v: f64) -> f64 {
|
|||
a.1 + (c.1 - a.1) * (v - a.0) / (c.0 - a.0).max(1e-9)
|
||||
}
|
||||
|
||||
/// Color Stabilizer's correction of buffer `b` as one piecewise-linear map per channel (points
|
||||
/// with increasing x; the end segments extend), the same maps [`color_stabilizer`] applies.
|
||||
/// `None` without a host or reference frame (the layer passes through). (The GPU effect.)
|
||||
pub fn color_stabilizer_maps(ctx: &EffectCtx, b: &Buf) -> Option<[Vec<(f64, f64)>; 3]> {
|
||||
let reference = ctx.env.host?.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index)?;
|
||||
let r = ctx.params.f("sampleSize").max(0.5);
|
||||
let pts = [ctx.params.v2("blackPoint"), ctx.params.v2("midPoint"), ctx.params.v2("whitePoint")];
|
||||
let cur = pts.map(|pt| sample_mean(b, pt, r));
|
||||
let refv = pts.map(|pt| sample_mean(&reference, pt, r));
|
||||
let mode = ctx.params.e("stabilize");
|
||||
Some([0, 1, 2].map(|k| {
|
||||
let offset = |c0: f64, r0: f64| vec![(0.0, r0 - c0), (1.0, 1.0 + r0 - c0)];
|
||||
match mode {
|
||||
0 => offset(cur[0][k], refv[0][k]),
|
||||
1 => {
|
||||
let (c0, c1, r0, r1) = (cur[0][k], cur[2][k], refv[0][k], refv[2][k]);
|
||||
if (c1 - c0).abs() < 1e-6 {
|
||||
offset(c0, r0)
|
||||
} else if c0 < c1 {
|
||||
vec![(c0, r0), (c1, r1)]
|
||||
} else {
|
||||
vec![(c1, r1), (c0, r0)]
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut v = vec![(0.0, 0.0)];
|
||||
let mut pairs: Vec<(f64, f64)> = (0..3).map(|i| (cur[i][k], refv[i][k])).collect();
|
||||
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
for (x, y) in pairs {
|
||||
if x > v.last().expect("non-empty").0 + 1e-4 && x < 1.0 - 1e-4 {
|
||||
v.push((x, y));
|
||||
}
|
||||
}
|
||||
v.push((1.0, 1.0));
|
||||
v
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn color_stabilizer(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let Some(host) = ctx.env.host else { return b };
|
||||
let Some(reference) = host.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index) else { return b };
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ pub(crate) const IDS: &[&str] = &[
|
|||
"ec.color.equalize",
|
||||
"ec.color.shadowhighlight",
|
||||
"ec.color.cccolorneutralizer",
|
||||
"ec.color.colorstabilizer",
|
||||
];
|
||||
|
||||
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||
|
|
@ -319,6 +320,25 @@ 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.colorstabilizer" => {
|
||||
// The sample means are measured on the CPU (the frame is read back).
|
||||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
let img = e.download(&b.img)?;
|
||||
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
|
||||
let Some(maps) = effectcraft_effects::color_stabilizer_maps(ctx, &cpu) else { return Some(b) };
|
||||
let mut d = vec![];
|
||||
for m in &maps {
|
||||
d.push(m.len() as f32);
|
||||
for i in 0..5 {
|
||||
let (x, y) = m.get(i).copied().unwrap_or((0.0, 0.0));
|
||||
d.extend([x as f32, y as f32]);
|
||||
}
|
||||
}
|
||||
p.u[0][0] = 24;
|
||||
data = Some(d);
|
||||
}
|
||||
"ec.color.cccolorneutralizer" => {
|
||||
let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")];
|
||||
for (i, (u, bal)) in pairs.iter().enumerate() {
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ fn fxt_eq_look(table: u32, n: u32, v: f32) -> f32 {
|
|||
// 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
|
||||
// 24 Color Stabilizer (data: per channel n, then 5 × (x, y))
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
|
|
@ -690,6 +691,29 @@ fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
|||
r = fxt_mix(r, c, P.f[3].y);
|
||||
o = vec4<f32>(r * a, a);
|
||||
}
|
||||
case 24u: {
|
||||
if (a <= 0.0) {
|
||||
break;
|
||||
}
|
||||
var r: vec3<f32>;
|
||||
for (var k = 0u; k < 3u; k++) {
|
||||
let base = k * 11u;
|
||||
let n = u32(data[base]);
|
||||
let v = c[k];
|
||||
// slice::partition_point(x < v), clamped to 1..n-1.
|
||||
var i = 0u;
|
||||
while (i < n && data[base + 1u + 2u * i] < v) {
|
||||
i++;
|
||||
}
|
||||
i = clamp(i, 1u, n - 1u);
|
||||
let ax = data[base + 1u + 2u * (i - 1u)];
|
||||
let ay = data[base + 2u + 2u * (i - 1u)];
|
||||
let cx = data[base + 1u + 2u * i];
|
||||
let cy = data[base + 2u + 2u * i];
|
||||
r[k] = ay + (cy - ay) * (v - ax) / max(cx - ax, 1e-9);
|
||||
}
|
||||
o = vec4<f32>(r * a, a);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
//! GPU colour-correction family effects vs the CPU effects (the oracle): direct on a buffer at
|
||||
//! full and half resolution, as adjustment, and composited at 8 and 32 bpc.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::{Keyframe, Value};
|
||||
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource, build};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::tests::{c, effect_case, n};
|
||||
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
|
|
@ -142,3 +146,40 @@ fn shadow_highlight_neutralizer_invert() {
|
|||
effect_case("ec.channel.invert", &[("channel", e(ch)), ("blend", n(20.0))]);
|
||||
}
|
||||
}
|
||||
|
||||
/// Color Stabilizer reads its reference frame through the effect host: a precomp whose content
|
||||
/// moves, at a nonzero time (the direct cases have no host and pass the layer through).
|
||||
#[test]
|
||||
fn color_stabilizer() {
|
||||
effect_case("ec.color.colorstabilizer", &[]);
|
||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||
for mode in [0, 1, 2] {
|
||||
let mut s = Scene::new(depth);
|
||||
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||
s.push(bg);
|
||||
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||
let mut a = s.footage(50, 34);
|
||||
let pos = a.props.prop_mut("transform/position").unwrap();
|
||||
pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(22.0, 18.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(38.0, 24.0))];
|
||||
let op = a.props.prop_mut("transform/opacity").unwrap();
|
||||
op.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(40.0))];
|
||||
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
||||
let vals = [
|
||||
("stabilize", e(mode)),
|
||||
("blackPoint", Value::Vec2([18.0, 20.0])),
|
||||
("midPoint", Value::Vec2([30.0, 18.0])),
|
||||
("whitePoint", Value::Vec2([42.0, 22.0])),
|
||||
("sampleSize", n(4.0)),
|
||||
];
|
||||
s.effect(&mut l, "ec.color.colorstabilizer", &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let t = Tick::from_seconds_f64(0.5);
|
||||
let label = format!("color stabilizer {mode} {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), t), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue