namespace ytLive.Services.Compositor; /// /// A region of the composed frame, blurred in place. The privacy layer (a Blur element on the /// Live scene) and the webcam's background blur share this arithmetic, so "half strength" means /// exactly the same blur in both places — the creator asked for consistency, and two copies of a /// radius curve is how two features quietly disagree. /// /// The algorithm is the webcam's: box-average downscale by /// , three separable box passes at (≈ a Gaussian), then /// blend back inside the mask. Working at ¼ resolution is what keeps a strong blur affordable. /// /// Region-scoped. Cost is proportional to the blur's own rectangle, not the frame, so /// a small privacy box costs almost nothing; the full-frame webcam case pays the full price. /// public static class RegionBlur { /// Blur work happens at 1/ resolution and is upsampled while /// blending — a strong blur at a fraction of the cost. public const int Downscale = 4; /// Box-blur radius (in downscaled pixels) at strength 0 and 1. The maximum was halved /// from 12 on 2026-10-09: the creator found full range too heavy, and the established tools /// blur far less (StreamFX defaults its box blur to 5). public const int MinRadius = 1; public const int MaxRadius = 6; /// Strength (0–1) → box radius. Squared so the low half of the slider stays gentle. public static int RadiusFor(double strength) { var s = double.IsFinite(strength) ? Math.Clamp(strength, 0, 1) : 0; return (int)Math.Round(MinRadius + (MaxRadius - MinRadius) * s * s); } /// /// Blur a rectangle of (BGRA, bytes per row) /// in place, inside a rectangular or elliptical mask. /// /// /// Optional per-buffer-pixel map (1 byte each) of areas a STRONGER blur already covered. Pixels /// marked there are skipped and never double-blurred, which is what makes overlapping blur /// layers read as "the strongest one wins" instead of stacking into something stronger than any /// single setting allows. The caller sorts blurs strongest-first and allocates this once. /// /// True when it actually blurred something. public static bool ApplyInPlace( byte[] buffer, int stride, int frameWidth, int frameHeight, int x, int y, int width, int height, double strength, bool round, byte[]? coverage = null) { if (buffer is null || width <= 0 || height <= 0) return false; // Clamp to the frame; the elements are draggable past the edges by design. var x0 = Math.Clamp(x, 0, frameWidth); var y0 = Math.Clamp(y, 0, frameHeight); var x1 = Math.Clamp(x + width, 0, frameWidth); var y1 = Math.Clamp(y + height, 0, frameHeight); var w = x1 - x0; var h = y1 - y0; if (w <= 0 || h <= 0) return false; var f = Downscale; var sw = (w + f - 1) / f; var sh = (h + f - 1) / f; if (sw <= 0 || sh <= 0) return false; var radius = RadiusFor(strength); // 1. Downscale by box-averaging f×f blocks (3 channels; alpha is preserved as-is). var small = new float[sw * sh * 3]; for (var sy = 0; sy < sh; sy++) { var py0 = y0 + sy * f; var py1 = Math.Min(y0 + h, py0 + f); for (var sx = 0; sx < sw; sx++) { var px0 = x0 + sx * f; var px1 = Math.Min(x0 + w, px0 + f); float b = 0, g = 0, r = 0; var n = 0; for (var py = py0; py < py1; py++) { var p = py * stride + px0 * 4; for (var px = px0; px < px1; px++, p += 4) { b += buffer[p]; g += buffer[p + 1]; r += buffer[p + 2]; n++; } } if (n == 0) n = 1; var o = (sy * sw + sx) * 3; small[o] = b / n; small[o + 1] = g / n; small[o + 2] = r / n; } } // 2. Three separable box passes ≈ a Gaussian (the webcam's exact shape). var scratch = new float[small.Length]; for (var pass = 0; pass < 3; pass++) { BoxHorizontal(small, scratch, sw, sh, radius); BoxVertical(scratch, small, sw, sh, radius); } // 3. Blend back inside the mask. Bilinear on the small grid so the blur edge is not blocky. var cx = w / 2.0; var cy = h / 2.0; var rx = Math.Max(1.0, w / 2.0); var ry = Math.Max(1.0, h / 2.0); for (var row = 0; row < h; row++) { var py = y0 + row; var syf = (row + 0.5) / f - 0.5; var sy0 = Math.Clamp((int)Math.Floor(syf), 0, sh - 1); var sy1 = Math.Clamp(sy0 + 1, 0, sh - 1); var ty = Math.Clamp(syf - sy0, 0, 1); // Ellipse test in region space, per row: half-width of the chord. var ny = (row + 0.5 - cy) / ry; var chord = round && Math.Abs(ny) < 1 ? rx * Math.Sqrt(1 - ny * ny) : rx; for (var col = 0; col < w; col++) { var px = x0 + col; var index = py * stride + px * 4; if (coverage is not null && coverage[py * frameWidth + px] != 0) continue; if (round) { var dx = col + 0.5 - cx; if (Math.Abs(dx) > chord) continue; } var sxf = (col + 0.5) / f - 0.5; var sx0 = Math.Clamp((int)Math.Floor(sxf), 0, sw - 1); var sx1 = Math.Clamp(sx0 + 1, 0, sw - 1); var tx = Math.Clamp(sxf - sx0, 0, 1); var i00 = (sy0 * sw + sx0) * 3; var i01 = (sy0 * sw + sx1) * 3; var i10 = (sy1 * sw + sx0) * 3; var i11 = (sy1 * sw + sx1) * 3; for (var c = 0; c < 3; c++) { var top = small[i00 + c] + (small[i01 + c] - small[i00 + c]) * tx; var bottom = small[i10 + c] + (small[i11 + c] - small[i10 + c]) * tx; var value = top + (bottom - top) * ty; buffer[index + c] = ClampByte((float)value); } if (coverage is not null) coverage[py * frameWidth + px] = 1; } } return true; } private static void BoxHorizontal(float[] src, float[] dst, int w, int h, int radius) { var window = radius * 2 + 1; for (var y = 0; y < h; y++) { var row = y * w * 3; for (var c = 0; c < 3; c++) { float sum = 0; for (var i = -radius; i <= radius; i++) sum += src[row + Math.Clamp(i, 0, w - 1) * 3 + c]; for (var x = 0; x < w; x++) { dst[row + x * 3 + c] = sum / window; var add = Math.Clamp(x + radius + 1, 0, w - 1); var drop = Math.Clamp(x - radius, 0, w - 1); sum += src[row + add * 3 + c] - src[row + drop * 3 + c]; } } } } private static void BoxVertical(float[] src, float[] dst, int w, int h, int radius) { var window = radius * 2 + 1; for (var x = 0; x < w; x++) { for (var c = 0; c < 3; c++) { float sum = 0; for (var i = -radius; i <= radius; i++) sum += src[Math.Clamp(i, 0, h - 1) * w * 3 + x * 3 + c]; for (var y = 0; y < h; y++) { dst[y * w * 3 + x * 3 + c] = sum / window; var add = Math.Clamp(y + radius + 1, 0, h - 1); var drop = Math.Clamp(y - radius, 0, h - 1); sum += src[add * w * 3 + x * 3 + c] - src[drop * w * 3 + x * 3 + c]; } } } } private static byte ClampByte(float value) => value <= 0 ? (byte)0 : value >= 255 ? (byte)255 : (byte)value; }