using ytLive.Helpers; using ytLive.Models; namespace ytLive.Services.BackgroundEffects; /// /// Applies a webcam's to one captured frame (TASK 51). /// Pure pixel work over the seam — WPF-free and thread-agnostic, /// so the camera worker thread can call it before the frame reaches the preview bitmap /// and the compositor. /// /// /// returns the SAME frame instance — a true /// passthrough, no copy. /// returns a copy whose alpha is the person /// mask (bilinear-upscaled), RGB untouched, IsOpaque = false — the compositor's /// straight-alpha blend then shows the layers below the webcam. /// is the next slice; until it lands it passes /// through. /// /// /// A segmenter that returns no mask (or throws) never drops the layer: the frame passes /// through and the failure is logged once. /// public sealed class BackgroundEffectProcessor { private readonly IPersonSegmenter _segmenter; private bool _loggedFailure; public BackgroundEffectProcessor(IPersonSegmenter segmenter) { _segmenter = segmenter; } public VideoFrame Apply(VideoFrame frame, BackgroundEffect effect) { if (effect != BackgroundEffect.Remove) return frame; SegmentationMask? mask; try { mask = _segmenter.Segment(frame); } catch (Exception ex) { LogFailureOnce($"{ex.GetType().Name}: {ex.Message}"); return frame; } if (mask == null || mask.Values.Length < mask.Width * mask.Height || mask.Width < 1 || mask.Height < 1) { LogFailureOnce("segmenter returned no usable mask"); return frame; } return ApplyRemove(frame, mask); } internal static VideoFrame ApplyRemove(VideoFrame frame, SegmentationMask mask) { var src = frame.BgraPixels; var dst = new byte[src.Length]; Buffer.BlockCopy(src, 0, dst, 0, src.Length); var w = frame.Width; var h = frame.Height; var stride = frame.Stride; for (var y = 0; y < h; y++) { var my = (y + 0.5f) * mask.Height / h - 0.5f; var row = y * stride; for (var x = 0; x < w; x++) { var mx = (x + 0.5f) * mask.Width / w - 0.5f; var a = SampleBilinear(mask, mx, my); var alpha = (int)(a * src[row + x * 4 + 3] + 0.5f); dst[row + x * 4 + 3] = (byte)Math.Clamp(alpha, 0, 255); } } return new VideoFrame(w, h, dst) { Epoch = frame.Epoch, IsOpaque = false }; } private static float SampleBilinear(SegmentationMask mask, float x, float y) { var x0 = (int)MathF.Floor(x); var y0 = (int)MathF.Floor(y); var fx = x - x0; var fy = y - y0; var x1 = Math.Clamp(x0 + 1, 0, mask.Width - 1); var y1 = Math.Clamp(y0 + 1, 0, mask.Height - 1); x0 = Math.Clamp(x0, 0, mask.Width - 1); y0 = Math.Clamp(y0, 0, mask.Height - 1); var v = mask.Values; var top = v[y0 * mask.Width + x0] * (1 - fx) + v[y0 * mask.Width + x1] * fx; var bottom = v[y1 * mask.Width + x0] * (1 - fx) + v[y1 * mask.Width + x1] * fx; return Math.Clamp(top * (1 - fy) + bottom * fy, 0f, 1f); } private void LogFailureOnce(string reason) { if (_loggedFailure) return; _loggedFailure = true; AppLog.Write($"BackgroundEffectProcessor: passing webcam through — {reason}"); } }