using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Xunit;
using ytLive.Models;
using ytLive.Services;
using ytLive.Services.BackgroundEffects;
namespace ytLive.Tests;
/// TASK 51 slice 1 — the background-effects engine.
public class BackgroundEffectEngineTests
{
private sealed class FakeSegmenter : IPersonSegmenter
{
private readonly SegmentationMask? _mask;
public int Calls;
public FakeSegmenter(SegmentationMask? mask) => _mask = mask;
public SegmentationMask? Segment(VideoFrame frame)
{
Calls++;
return _mask;
}
}
/// The integration test for this unit: a webcam frame run through the
/// processor with Remove comes back with the person kept and the background
/// transparent, colors untouched, flagged non-opaque so the compositor blends it.
/// None is a true passthrough (same instance, segmenter never called).
[Fact]
public void Remove_keeps_the_person_and_makes_the_background_transparent()
{
const int w = 64, h = 32;
var pixels = new byte[w * h * 4];
for (var i = 0; i < pixels.Length; i += 4)
{
pixels[i] = 10; pixels[i + 1] = 20; pixels[i + 2] = 30; pixels[i + 3] = 255;
}
var frame = new VideoFrame(w, h, pixels) { IsOpaque = true };
// 4×2 mask: left half person, right half background.
var mask = new SegmentationMask(4, 2, new[] { 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f });
var segmenter = new FakeSegmenter(mask);
var processor = new BackgroundEffectProcessor(segmenter);
Assert.Same(frame, processor.Apply(frame, BackgroundEffect.None));
Assert.Equal(0, segmenter.Calls);
var result = processor.Apply(frame, BackgroundEffect.Remove);
Assert.NotSame(frame, result);
Assert.False(result.IsOpaque);
Assert.Equal(w, result.Width);
Assert.Equal(h, result.Height);
for (var y = 0; y < h; y++)
{
Assert.Equal(255, Alpha(result, 4, y)); // well inside the person half
Assert.Equal(0, Alpha(result, w - 5, y)); // well inside the background half
}
for (var i = 0; i < pixels.Length; i += 4)
{
Assert.Equal(pixels[i], result.BgraPixels[i]);
Assert.Equal(pixels[i + 1], result.BgraPixels[i + 1]);
Assert.Equal(pixels[i + 2], result.BgraPixels[i + 2]);
}
Assert.Equal(255, frame.BgraPixels[(w - 5) * 4 + 3]); // source frame never mutated
}
/// Model contract: the BUNDLED (embedded) ONNX model, driven through the C#
/// preprocessing, reproduces Google's own MediaPipe runtime on a fixed fixture.
/// The expected mask was produced by tools/selfie-seg/ running the original .tflite in
/// MediaPipe (fixture: MediaPipe's public test image mozart_square.jpg, a public-domain
/// painting, cropped to 256×144). Guards the embedding, the input layout (RGB, NHWC,
/// /255) and the conversion itself.
[Fact]
public void Bundled_model_matches_the_MediaPipe_reference_mask()
{
var frame = LoadBgra(Fixture("selfie-fixture-256x144.png"));
var expected = LoadBgra(Fixture("selfie-fixture-expected-mask.png"));
using var segmenter = new OnnxPersonSegmenter();
var mask = segmenter.Segment(frame);
Assert.NotNull(mask);
Assert.Equal(OnnxPersonSegmenter.ModelWidth, mask!.Width);
Assert.Equal(OnnxPersonSegmenter.ModelHeight, mask.Height);
var agree = 0;
var person = 0;
for (var i = 0; i < mask.Width * mask.Height; i++)
{
var expectPerson = expected.BgraPixels[i * 4] > 127;
var gotPerson = mask.Values[i] > 0.5f;
if (expectPerson == gotPerson) agree++;
if (gotPerson) person++;
}
var total = mask.Width * mask.Height;
Assert.True(agree >= total * 0.995, $"binary agreement {100.0 * agree / total:0.00}% < 99.5%");
Assert.InRange(100.0 * person / total, 45, 60); // reference: 52.5% person
}
/// Slice 2 integration test: Blur keeps the person pixel-for-pixel and smooths the
/// background — a 1px black/white checkerboard behind the "person" comes back as mid-tones,
/// opaque, same size.
[Fact]
public void Blur_keeps_the_person_sharp_and_smooths_the_background()
{
const int w = 64, h = 32;
var pixels = new byte[w * h * 4];
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++)
{
var p = (y * w + x) * 4;
if (x < w / 2)
{
pixels[p] = 100; pixels[p + 1] = 150; pixels[p + 2] = 200;
}
else
{
var v = (byte)(((x + y) & 1) == 0 ? 0 : 255);
pixels[p] = v; pixels[p + 1] = v; pixels[p + 2] = v;
}
pixels[p + 3] = 255;
}
var frame = new VideoFrame(w, h, pixels) { IsOpaque = true };
var mask = new SegmentationMask(4, 2, new[] { 1f, 1f, 0f, 0f, 1f, 1f, 0f, 0f });
var processor = new BackgroundEffectProcessor(new FakeSegmenter(mask));
var result = processor.Apply(frame, BackgroundEffect.Blur);
Assert.NotSame(frame, result);
Assert.True(result.IsOpaque);
Assert.Equal(w, result.Width);
Assert.Equal(h, result.Height);
for (var y = 0; y < h; y++)
{
for (var x = 0; x < 24; x++) // person: untouched
for (var c = 0; c < 4; c++)
Assert.Equal(pixels[(y * w + x) * 4 + c], result.BgraPixels[(y * w + x) * 4 + c]);
for (var x = 44; x < w; x++) // background: checkerboard smoothed away
{
var p = (y * w + x) * 4;
for (var c = 0; c < 3; c++)
Assert.InRange(result.BgraPixels[p + c], (byte)30, (byte)225);
Assert.Equal(255, result.BgraPixels[p + 3]);
}
}
}
/// TASK 51 blur-heavy fix — the integration test: an all-background frame with one
/// bright pixel. The default 0.5 strength must keep the halo tight (quadratic curve: radius 2
/// small px) while full strength spreads it far (radius 6) — the maker complained the default
/// was heavy under the old linear 1..12 range.
[Fact]
public void Blur_default_strength_is_gentle_and_full_strength_is_strong()
{
const int w = 128, h = 64;
var pixels = new byte[w * h * 4];
for (var y = 0; y < h; y++)
for (var x = 0; x < w; x++)
{
var p = (y * w + x) * 4;
if (x < w / 2) pixels[p] = 200;
pixels[p + 1] = 180;
pixels[p + 2] = 160;
pixels[p + 3] = 255;
}
var frame = new VideoFrame(w, h, pixels) { IsOpaque = true };
// All background — the whole frame blurs, no person boundary to confuse the edge spread.
var mask = new SegmentationMask(4, 2, new float[8]);
var processor = new BackgroundEffectProcessor(new FakeSegmenter(mask));
var gentle = processor.Apply(frame, BackgroundEffect.Blur, BackgroundEffectProcessor.DefaultStrength);
var strong = processor.Apply(frame, BackgroundEffect.Blur, 1.0);
// The radius mapping itself: quadratic, not the old linear 1..12. Default 0.5 = 2 small px
// (was 6), max = 6 small px (was 12) — the reason the default felt heavy before.
Assert.Equal(1, BackgroundEffectProcessor.BlurRadius(0.0));
Assert.Equal(2, BackgroundEffectProcessor.BlurRadius(BackgroundEffectProcessor.DefaultStrength));
Assert.Equal(BackgroundEffectProcessor.MinBlurRadius, BackgroundEffectProcessor.BlurRadius(0.0));
Assert.Equal(BackgroundEffectProcessor.MaxBlurRadius, BackgroundEffectProcessor.BlurRadius(1.0));
// Behaviour at a step edge: the default smears light only ~9px into the dark half (measured),
// full strength smears ~26px — a gentle default with a real range above it.
var (reachGentle, sumGentle) = EdgeReach(gentle);
var (reachStrong, sumStrong) = EdgeReach(strong);
Assert.True(reachGentle <= 12, $"default blur reach {reachGentle}px (expected <= 12)");
Assert.True(reachStrong >= 20, $"max blur reach {reachStrong}px (expected >= 20)");
Assert.True(reachStrong > reachGentle);
Assert.True(sumStrong > sumGentle * 2, "max strength should smear visibly more light than default");
}
/// Largest x into the dark half (x = w/2) still carrying light, and the total light
/// in the dark half — the blur's smearing distance at a step edge.
private static (int Reach, double Sum) EdgeReach(VideoFrame f)
{
var edge = f.Width / 2;
var reach = 0;
double sum = 0;
for (var y = 0; y < f.Height; y++)
for (var x = edge; x < f.Width; x++)
{
var v = f.BgraPixels[(y * f.Width + x) * 4];
if (v >= 32) reach = Math.Max(reach, x - edge);
sum += v;
}
return (reach, sum);
}
private static int Alpha(VideoFrame f, int x, int y) => f.BgraPixels[(y * f.Width + x) * 4 + 3];
private static string Fixture(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name);
private static VideoFrame LoadBgra(string path)
{
using var stream = File.OpenRead(path);
var decoded = BitmapDecoder.Create(stream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad).Frames[0];
var bgra = new FormatConvertedBitmap(decoded, PixelFormats.Bgra32, null, 0);
var pixels = new byte[bgra.PixelWidth * bgra.PixelHeight * 4];
bgra.CopyPixels(pixels, bgra.PixelWidth * 4, 0);
return new VideoFrame(bgra.PixelWidth, bgra.PixelHeight, pixels);
}
}