Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.x
dotnet-version: 10.x
- name: Install versionize
run: dotnet tool install --global Versionize
- name: Setup git
Expand All @@ -36,7 +36,7 @@ jobs:
continue-on-error: true
- name: Upload changelog
if: steps.versionize.outcome == 'success'
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: change-log
path: src/BGR.Console/CHANGELOG.md
Expand All @@ -55,15 +55,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.ref }}
token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.x
dotnet-version: 10.x
- name: Build
run: dotnet build src
- name: Publish for mac-os
Expand All @@ -84,12 +84,12 @@ jobs:
with:
proj-path: src/BGR.Console/BGR.Console.csproj
- name: Download changlog
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: change-log
path: src/BGR.Console
- name: Create release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
with:
token: ${{ secrets.ACTIONS_PAT }}
name: bgr v${{ steps.get-version.outputs.version }}
Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ jobs:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET
uses: actions/setup-dotnet@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.x.x
dotnet-version: 10.x.x
- name: Format project
run: dotnet format src --verbosity normal
- name: Commit Changes
Expand All @@ -49,14 +49,14 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9
uses: actions/setup-dotnet@v4
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 9.x
dotnet-version: 10.x
- name: Install report generator
run: dotnet tool install --global dotnet-reportgenerator-globaltool
- name: Restore dependencies
Expand All @@ -68,7 +68,7 @@ jobs:
- name: Rename test coverage report
run: mv src/BGR.Console.Tests/TestResults/Coverage/coverage.cobertura.xml src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml
- name: Upload test coverage report for ${{ matrix.os }}
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: test-coverage-${{ matrix.os }}
path: src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml
Expand All @@ -78,26 +78,26 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v7
with:
fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }}
- name: Download ubuntu-latest report
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: test-coverage-ubuntu-latest
path: ./coverage
- name: Download windows-latest report
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: test-coverage-windows-latest
path: ./coverage
- name: Download macos-latest report
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: test-coverage-macos-latest
path: ./coverage
- name: Upload test coverage reports
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ bgr --help

![Output Image](examples/output.png)

### Feathering Options

By default, BGR applies feathering to create smooth transitions between the foreground and background, reducing harsh edges and halos. You can control the feathering range using two options:

- `--feather-min` - Mask values below this threshold become fully transparent. Default: `70`
- `--feather-max` - Mask values above this threshold become fully opaque. Default: `117`

Values between `feather-min` and `feather-max` are linearly scaled to produce partial transparency, creating a smooth gradient at edges.

```pwsh
bgr /path/to/image.jpg --feather-min 60 --feather-max 130
```

A wider range (e.g., `50` to `150`) produces softer edges, while a narrower range (e.g., `90` to `100`) produces sharper edges.

## Issues

If you encounter any issues while using the app, please open an issue on the repository. If you have any suggestions or feature requests, feel free to open an issue as well.
Expand Down
5 changes: 5 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"sdk": {
"rollForward": "latestFeature"
}
}
11 changes: 6 additions & 5 deletions src/BGR.Console.Tests/BGR.Console.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,26 @@

<PropertyGroup>
<IsPackable>false</IsPackable>
<NoWarn>1591;</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.msbuild" Version="6.0.4">
<PackageReference Include="coverlet.msbuild" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="Spectre.Console.Testing" Version="0.49.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="Spectre.Console.Testing" Version="0.57.2" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
36 changes: 35 additions & 1 deletion src/BGR.Console.Tests/Unit/ImageSharpProcessorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,10 @@ public async Task RemoveBackgroundAsync_WithValidImageAndMask_ShouldReturnProces
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);
Expand All @@ -203,6 +206,37 @@ public async Task RemoveBackgroundAsync_WithValidImageAndMask_ShouldReturnProces
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()
{
Expand Down
2 changes: 0 additions & 2 deletions src/BGR.Console.Tests/Unit/OnnxTensorTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using BGR.Console.Removal.Onnx;

using Microsoft.ML.OnnxRuntime.Tensors;

namespace BGR.Console.Tests.Unit;
Expand Down
9 changes: 4 additions & 5 deletions src/BGR.Console.Tests/Unit/RemovalCommandTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using Microsoft.Extensions.Logging;

using Spectre.Console.Cli;
using System.Diagnostics.CodeAnalysis;

namespace BGR.Console.Tests.Unit;

Expand Down Expand Up @@ -28,6 +26,7 @@ public RemovalCommandTests()
[Theory]
[InlineData("output.png", false)]
[InlineData("", true)]
[SuppressMessage("Reliability", "CA2025:Do not pass 'IDisposable' instances into unawaited tasks", Justification = "Matching invocations")]
public async Task ExecuteAsync_WhenCalled_ItShouldProcessImage(string outputPath, bool includeMask)
{
var imagePath = $"{Guid.NewGuid()}.png";
Expand Down Expand Up @@ -73,7 +72,7 @@ public async Task ExecuteAsync_WhenCalled_ItShouldProcessImage(string outputPath
.ReturnsAsync(maskStream);

_imageProcessorMock
.Setup(p => p.RemoveBackgroundAsync(image.Data, maskStream))
.Setup(p => p.RemoveBackgroundAsync(image.Data, maskStream, It.IsAny<byte>(), It.IsAny<byte>()))
.ReturnsAsync(outputStream);

var commandContext = new CommandContext(
Expand All @@ -92,7 +91,7 @@ public async Task ExecuteAsync_WhenCalled_ItShouldProcessImage(string outputPath
_imageProcessorMock.Verify(p => p.CreateTensorInputAsync(image.Data, model), Times.Once);
_inferenceRunnerMock.Verify(r => r.Run(model.Bytes, inputTensor), Times.Once);
_imageProcessorMock.Verify(p => p.GenerateMaskAsync(outputTensor, image.Width, image.Height), Times.Once);
_imageProcessorMock.Verify(p => p.RemoveBackgroundAsync(image.Data, maskStream), Times.Once);
_imageProcessorMock.Verify(p => p.RemoveBackgroundAsync(image.Data, maskStream, It.IsAny<byte>(), It.IsAny<byte>()), Times.Once);
_imageProcessorMock.Verify(p => p.SaveImageAsync(outputStream, It.IsAny<string>()), Times.AtLeastOnce);

File.Delete(imagePath);
Expand Down
20 changes: 10 additions & 10 deletions src/BGR.Console/BGR.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.27.1" />
<PackageReference Include="Serilog" Version="4.4.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" />
<PackageReference Include="Spectre.Console" Version="0.49.1" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Spectre.Console" Version="0.57.2" />
<PackageReference Include="Spectre.Console.Cli" Version="0.55.0" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion src/BGR.Console/Removal/ImageProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ internal abstract class ImageProcessor

public abstract Task<Stream> GenerateMaskAsync(ITensor<float> maskTensor, int width, int height);

public abstract Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask);
public abstract Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask, byte featherMin, byte featherMax);

public abstract Task SaveImageAsync(Stream image, string path);

Expand Down
35 changes: 21 additions & 14 deletions src/BGR.Console/Removal/ImageSharp/ImageSharpProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public override async Task<Stream> GenerateMaskAsync(ITensor<float> maskTensor,
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;
Expand All @@ -76,19 +76,14 @@ public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream ma
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();
Expand All @@ -107,12 +102,7 @@ public override async Task SaveImageAsync(Stream image, string path)

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)
Expand All @@ -129,4 +119,21 @@ private static float CalculateSigmoid(float x)

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;
}

var proportion = (maskValue - minVal) / (float)(maxVal - minVal);

return (byte)(proportion * 255f);
}
}
Loading