Luma Key and Extract: Rec. 601 luminance, as After Effects

After Effects 2026 keys Luma Key and Extract (Channel Luminance) on Rec. 601
weights of the working-space values: Key Out Darker / Extract Black Point at
65, 166 and 24 on pure red, green and blue keep red (601: 76, 709: 54) and
blue (29 / 18) and key out green (150 / 182) - Rec. 709, which EffectCraft
used, decides all three the other way. Case 904 E18 (Extract 30 / 220 /
softness 10 on grey and colour steps): 513 of 5184 grid points off (pure blue
kept at 231 in AE, keyed out in EC), now 0, max 0. CPU and GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-19 16:16:01 +02:00
co-authored by Claude Opus 5.5
parent 39bd0efac2
commit 75271bfe6c
4 changed files with 35 additions and 13 deletions
+8 -7
View File
@@ -14,13 +14,14 @@ fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
}
/// Rec. 601 luminance on the working-space values, as After Effects 2026 renders Tint, Tritone
/// and the other Rec. 601 effects (sRGB working space, not linearised: red 76/255, green
/// 150/255, blue 29/255). Tint and Tritone use it (Tritone, case 718: a red solid with
/// Blend With Original 25 gives (132, 23, 46), from luminance 0.299; Rec. 709's 0.213 gave
/// (113, 16, 49); on a grey ramp both weightings agree, case 815). The GPU kernels
/// (`pointwise` op 1, `fx_color` op 4) mirror it.
fn luma_601(c: [f32; 3]) -> f32 {
/// Rec. 601 luminance on the working-space values (sRGB working space, not linearised: red
/// 76/255, green 150/255, blue 29/255), as After Effects 2026 renders Tint, Tritone, Luma Key and
/// Extract. Tritone, case 718: a red solid with Blend With Original 25 gives (132, 23, 46), from
/// luminance 0.299; Rec. 709's 0.213 gave (113, 16, 49); on a grey ramp both weightings agree
/// (case 815). Luma Key and Extract: Key Out Darker / Extract Black Point at 65, 166 and 24 on
/// pure red, green and blue keep red (601: 76, 709: 54) and blue (29 / 18) and key out green
/// (150 / 182). The GPU kernels (`pointwise` op 1, `fx_color` op 4, `fxk_key_luma`) mirror it.
pub(crate) fn luma_601(c: [f32; 3]) -> f32 {
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
}
+18 -3
View File
@@ -2,12 +2,13 @@
//! matte clean-up. Written from public behaviour descriptions and the standard colour-difference /
//! blue-screen matting literature (Vlahos; Smith & Blinn, "Blue Screen Matting").
use effectcraft_color::{luminance, rgb_to_hsl, srgb_to_linear};
use effectcraft_color::{rgb_to_hsl, srgb_to_linear};
use effectcraft_keyframe::Value;
use effectcraft_project::ParamUi;
use effectcraft_raster::{Image, gaussian_blur};
use rayon::prelude::*;
use crate::color_fx::luma_601;
use crate::util::{Plane, gauss_plane, morph_frac, premul, unpremul};
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
@@ -144,7 +145,7 @@ fn luma_key(ctx: &EffectCtx, mut b: Buf) -> Buf {
let thr = ctx.params.f("threshold") as f32 / 255.0;
let tol = ctx.params.f("tolerance") as f32 / 255.0;
let m = matte_of(&b.img, |c, _| {
let l = luminance(c[0], c[1], c[2]);
let l = luma_601([c[0], c[1], c[2]]);
let keep = match kind {
0 => l <= thr,
2 => (l - thr).abs() > tol,
@@ -311,7 +312,7 @@ fn extract(ctx: &EffectCtx, mut b: Buf) -> Buf {
2 => c[1],
3 => c[2],
4 => a,
_ => luminance(c[0], c[1], c[2]),
_ => luma_601([c[0], c[1], c[2]]),
};
let mut k = 1.0f32;
if v < bp {
@@ -827,6 +828,20 @@ mod tests {
}
}
/// After Effects 2026: Key Out Darker and Extract's Black Point at 65 / 166 / 24 on pure
/// red / green / blue keep red and blue and key out green: Rec. 601 luminance (Rec. 709
/// would key out red and blue and keep green).
#[test]
fn luma_key_and_extract_use_rec601_as_after_effects() {
for (c, thr, kept) in [([1.0, 0.0, 0.0], 65.0, true), ([0.0, 1.0, 0.0], 166.0, false), ([0.0, 0.0, 1.0], 24.0, true)] {
let img = Image::filled(2, 1, [c[0], c[1], c[2], 1.0]);
let key = run("ec.key.luma", &[("keyType", Value::Enum(1)), ("threshold", num(thr))], img.clone());
let ext = run("ec.key.extract", &[("blackPoint", num(thr))], img);
let want = if kept { 1.0 } else { 0.0 };
assert_eq!((key.get(0, 0)[3], ext.get(0, 0)[3]), (want, want), "{c:?} at {thr}");
}
}
#[test]
fn extract_keeps_range() {
let mut img = Image::new(3, 1);
+7 -2
View File
@@ -62,6 +62,11 @@ fn fxk_range_components(space: u32, c: vec3<f32>) -> vec3<f32> {
}
// keying::matte_levels.
// Luma Key / Extract luminance: Rec. 601 (effects::color_fx::luma_601).
fn fxk_key_luma(c: vec3<f32>) -> f32 {
return dot(c, vec3<f32>(0.299, 0.587, 0.114));
}
fn fxk_matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 {
var t = clamp((v - in_b / 255.0) / max((in_w - in_b) / 255.0, 1e-4), 0.0, 1.0);
t = powz(t, 1.0 / max(gamma, 0.01));
@@ -195,7 +200,7 @@ fn fxk_point(@builtin(global_invocation_id) gid: vec3<u32>) {
o = vec4<f32>(select(1.0, 0.0, d <= f0.w));
}
case 2u: {
let l = luminance(c);
let l = fxk_key_luma(c);
let thr = f0.x;
let tol = f0.y;
var m: f32;
@@ -231,7 +236,7 @@ fn fxk_point(@builtin(global_invocation_id) gid: vec3<u32>) {
case 2u: { v = c.y; }
case 3u: { v = c.z; }
case 4u: { v = a; }
default: { v = luminance(c); }
default: { v = fxk_key_luma(c); }
}
v = 255.0 * v;
let bp = f0.x;
+2 -1
View File
@@ -35,7 +35,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
| Expression Controls | 9 | 9 | — |
| Generate | 26 | 25 | Fractal on the CPU (needs f64); #526 (Reset) and #536 (Write-on marks) are closed |
| Immersive Video | 12 | 12 | — |
| Keying | 12 | 12 | Key Cleaner approximated; #533 (wire-removal Frame Offset) is closed |
| Keying | 12 | 12 | Luma Key and Extract key on Rec. 601 luminance, as After Effects 2026; Key Cleaner approximated; #533 (wire-removal Frame Offset) is closed |
| Matte | 6 | 4 | Mocha shape reads our documented JSON, not Mocha's format |
| Noise & Grain | 13 | 12 | Curl Noise (#463); Match Grain on the CPU |
| Obsolete | 9 | 9 | Luma Key matches After Effects 2026 on a grey ramp (hard cut, Tolerance only for Similar / Dissimilar) |
@@ -69,6 +69,7 @@ the GPU is performance, not parity.
| 2026-10-19 | minor | Levels: "Off for 32 bpc Color" no longer clips in 32 bpc projects, unclipped values extrapolate below Output Black; Levels (Individual Controls) applies the channel controls before the RGB ones, measured against After Effects 2026 (CPU and GPU) |
| 2026-10-19 | minor | Brightness & Contrast: negative Brightness / Contrast are the inverse curves of the positive ones (Brightness −40: 20/255 → 6/255 off After Effects 2026), CPU and GPU |
| 2026-10-19 | minor | Luma Key: Key Out Brighter / Darker cut hard at Threshold, measured against After Effects 2026 renders of a ramp (CPU and GPU) |
| 2026-10-19 | minor | Luma Key and Extract: Rec. 601 luminance (was Rec. 709), measured against After Effects 2026 (CPU and GPU) |
| 2026-10-18 | minor | Tint uses Rec. 601 luminance weights, measured against After Effects 2026 renders of red, green, blue and grey solids (CPU and GPU) |
| 2026-10-18 | minor | Exposure converts to linear light with a pure 2.2 power curve, as measured in After Effects (Offset 0.0005 / 0.002 / 0.02 / 0.1 on black: 8 / 15 / 43 / 90), not the piecewise sRGB curve; its sliders take After Effects' ranges (Exposure −100..100, slider −4..4; Offset −2..2, slider −0.5..0.5; Gamma Correction 0.1..10) |
| 2026-10-12 | minor | Parameters 3,014 to 3,017 (effects.md); #303, #514, #526, #533–#536 marked closed instead of pending |