Files
LlamaCasty/ytLive.Tests/BackgroundEffectEngineTests.cs
T

164 lines
6.8 KiB
C#
Raw 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.
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;
/// <summary>TASK 51 slice 1 — the background-effects engine.</summary>
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;
}
}
/// <summary>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).</summary>
[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
}
/// <summary>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.</summary>
[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
}
/// <summary>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.</summary>
[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]);
}
}
}
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);
}
}