Files

68 lines
2.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
namespace ytLive.Services.BackgroundEffects;
/// <summary>
/// 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 = <see cref="CurrentWeight"/> × new + (1 − <see cref="CurrentWeight"/>) × 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 <see cref="CameraManager"/>'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.
/// </summary>
public sealed class MaskSmoother
{
/// <summary>Weight of the newest raw mask; the rest is the previous smoothed mask.</summary>
internal const double CurrentWeight = 0.75;
private float[]? _current;
private float[]? _scratch;
private int _width;
private int _height;
private bool _hasHistory;
/// <summary>
/// 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.
/// </summary>
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);
}
}