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
@@ -67,7 +67,7 @@ internal class ImageSharpProcessor : ImageProcessor
return stream;
}
public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask)
public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask, byte featherMin, byte featherMax)
{
image.Position = 0;
mask.Position = 0;
@@ -76,19 +76,14 @@ internal class ImageSharpProcessor : ImageProcessor
var maskImage = await Image.LoadAsync<Rgba32>(mask);
using var imageWithBgRemoved = new Image<Rgba32>(imageWithBg.Width, imageWithBg.Height);
const byte alphaThreshold = 20;
var transparentPixel = new Rgba32(0, 0, 0, 0);
WalkImage(imageWithBg.Height, imageWithBg.Width, (x, y) =>
{
var sourcePixel = imageWithBg[x, y];
var maskPixel = maskImage[x, y];
var alpha = maskPixel.R;
var alpha = AdjustAlpha(maskPixel.R, featherMin, featherMax);
imageWithBgRemoved[x, y] = alpha > alphaThreshold
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
: transparentPixel;
imageWithBgRemoved[x, y] = new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, alpha);
});
var result = new MemoryStream();
@@ -107,12 +102,7 @@ internal class ImageSharpProcessor : ImageProcessor
private static float Normalize(float value)
{
const float binarizationThreshold = 0.5f;
const float normalizationFactor = 2f;
return value > binarizationThreshold
? (value - binarizationThreshold) * normalizationFactor
: 0f;
return value * value;
}
private static byte ConvertToGreyscale(float value)
@@ -129,4 +119,15 @@ internal class ImageSharpProcessor : ImageProcessor
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
}
private static byte AdjustAlpha(byte maskValue, byte minVal, byte maxVal)
{
if (maskValue <= minVal)
return 0;
if (maskValue >= maxVal)
return 255;
float proportion = (maskValue - minVal) / (float)(maxVal - minVal);
return (byte)(proportion * 255f);
}
}