namespace ytLive.Services.BackgroundEffects; /// /// Temporal smoothing for the person mask (TASK 51): each raw frame's segmentation is blended /// with the previous smoothed mask so the cut-out edge doesn't flutter from frame to frame when /// the model's per-frame verdict wavers. /// /// Blend = × new + (1 − ) × previous. /// Weights come from the established tools rather than a guess: OBS's obs-backgroundremoval /// smoke-tests `temporal_smooth_factor` at its 0.85 default, and Jitsi's webcam background /// filter (jitsi/jitsi-meet#17080) found 0.7/0.3 transformed best — a midpoint that still /// reacts to real movement. 0.5/0.5 is worse: on motion it leaves a smearing edge ghost. /// /// State lives per camera session (the 's one-per-frame worker), NOT /// in the segmenter — the segmenter is shared app-wide and its contract test pins the raw /// per-frame mask. /// public sealed class MaskSmoother { /// Weight of the newest raw mask; the rest is the previous smoothed mask. internal const double CurrentWeight = 0.75; private float[]? _current; private float[]? _scratch; private int _width; private int _height; private bool _hasHistory; /// /// Returns the temporally-smoothed mask. A null input (segmenter hiccup) resets history so /// the next real mask starts moving again; the first frame and any size change pass through /// un-blended, and the returned mask never aliases the caller's array (the first frame copies /// its history). The two buffers ping-pong so no per-frame allocation is needed — a 256×144 /// mask is 147 KB, past the 85 KB LOH threshold. /// public SegmentationMask? Smooth(SegmentationMask? raw) { if (raw == null) { _hasHistory = false; _width = 0; _height = 0; return null; } if (!_hasHistory || _width != raw.Width || _height != raw.Height || _current == null || _scratch == null) { _width = raw.Width; _height = raw.Height; _current = new float[raw.Values.Length]; _scratch = new float[raw.Values.Length]; Array.Copy(raw.Values, _current, raw.Values.Length); _hasHistory = true; return raw; } var v = raw.Values; var next = _scratch; var keep = 1f - (float)CurrentWeight; for (var i = 0; i < v.Length; i++) next[i] = keep * _current[i] + (float)CurrentWeight * v[i]; _scratch = _current; _current = next; return new SegmentationMask(_width, _height, next); } }