68 lines
2.7 KiB
C#
68 lines
2.7 KiB
C#
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);
|
||
}
|
||
} |