feat: improve background removal feathering

- replace hard binary thresholding with smooth
  linear alpha mapping to reduce halos around
  foreground edges especially in low
  resolution images
- normalize now uses a quadratic curve instead
  of hard binarization so that gradient info
  from the model output is preserved
- removebackgroundasync applies a configurable
  linear interpolation between the set min and
  max feathering thresholds so you have partial
  transparency at the edges
This commit is contained in:
Stevan Freeborn
2026-07-15 10:52:35 -05:00
parent 51abe75e07
commit e099144127
5 changed files with 62 additions and 19 deletions
@@ -183,7 +183,10 @@ public class ImageSharpProcessorTests : IDisposable
await mask.SaveAsPngAsync(maskStream);
maskStream.Position = 0;
var result = await _sut.RemoveBackgroundAsync(imageStream, maskStream);
const byte featherMin = 70;
const byte featherMax = 117;
var result = await _sut.RemoveBackgroundAsync(imageStream, maskStream, featherMin, featherMax);
result.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0);
@@ -203,6 +206,37 @@ public class ImageSharpProcessorTests : IDisposable
resultImage[1, 1].A.ShouldBe((byte)255);
}
[Fact]
public async Task RemoveBackgroundAsync_WithMidRangeMaskValue_ShouldApplyPartialAlpha()
{
using var imageStream = new MemoryStream();
using var image = new Image<Rgba32>(1, 1);
image[0, 0] = new Rgba32(100, 150, 200, 255);
await image.SaveAsPngAsync(imageStream);
imageStream.Position = 0;
using var maskStream = new MemoryStream();
using var mask = new Image<Rgba32>(1, 1);
mask[0, 0] = new Rgba32(100, 100, 100, 255);
await mask.SaveAsPngAsync(maskStream);
maskStream.Position = 0;
const byte featherMin = 70;
const byte featherMax = 117;
var result = await _sut.RemoveBackgroundAsync(imageStream, maskStream, featherMin, featherMax);
result.Position = 0;
using var resultImage = await Image.LoadAsync<Rgba32>(result);
resultImage[0, 0].R.ShouldBe((byte)100);
resultImage[0, 0].G.ShouldBe((byte)150);
resultImage[0, 0].B.ShouldBe((byte)200);
var expectedAlpha = (byte)((100 - 70) / (float)(117 - 70) * 255f);
resultImage[0, 0].A.ShouldBe(expectedAlpha);
}
[Fact]
public async Task SaveImageAsync_WhenCalled_ItShouldSaveImageToDiskAtProvidedPath()
{