16 Commits
Author SHA1 Message Date
Stevan Freeborn a95979a1a7 chore(release): 0.1.0 [skip ci] 2026-07-15 17:15:31 +00:00
Stevan Freeborn 79931087a8 Merge pull request #3 from StevanFreeborn/stevanfreeborn/feat/address-feathering
feat: address feathering
2026-07-15 12:15:09 -05:00
Stevan Freeborn 8269f21f55 chore: revert workflow troubleshooting 2026-07-15 11:50:27 -05:00
Stevan Freeborn b2bdc1d862 chore: add diagnostic to workflows 2026-07-15 11:39:31 -05:00
Stevan Freeborn 6160bf05d4 chore: update workflows to proper versions 2026-07-15 11:31:42 -05:00
Stevan Freeborn 6d03eefe45 style: correct code style 2026-07-15 10:55:20 -05:00
Stevan Freeborn e06d77c1d2 docs: updated README.md to include info about feathering options 2026-07-15 10:53:34 -05:00
Stevan Freeborn e099144127 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
2026-07-15 10:52:35 -05:00
Stevan Freeborn 51abe75e07 chore: add repo specific global.json 2026-07-15 10:47:26 -05:00
Stevan Freeborn 45dc9de223 feat: upgrade to .NET 10 2026-07-15 10:12:56 -05:00
Stevan Freeborn 97fa55e60a docs: update README.md [skip ci] 2025-02-14 02:36:49 -06:00
Stevan Freeborn 1117223cff docs; update README.md [skip ci] 2025-02-14 02:03:55 -06:00
Stevan Freeborn faad90645d docs: update readme.md [skip ci] 2025-02-14 01:54:46 -06:00
Stevan Freeborn f176893d1b docs: update readme [skip ci] 2025-02-14 01:39:54 -06:00
Stevan Freeborn 28a6e8aeb3 chore: update readme and update workflow [skip ci] 2025-02-14 01:38:50 -06:00
Stevan Freeborn 4067ef6c34 chore: fix changelog [skip ci] 2025-02-14 00:30:43 -06:00
15 changed files with 172 additions and 85 deletions
+22 -20
View File
@@ -16,14 +16,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 9.x dotnet-version: 10.x
- name: Install versionize - name: Install versionize
run: dotnet tool install --global Versionize run: dotnet tool install --global Versionize
- name: Setup git - name: Setup git
@@ -36,7 +36,7 @@ jobs:
continue-on-error: true continue-on-error: true
- name: Upload changelog - name: Upload changelog
if: steps.versionize.outcome == 'success' if: steps.versionize.outcome == 'success'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: change-log name: change-log
path: src/BGR.Console/CHANGELOG.md path: src/BGR.Console/CHANGELOG.md
@@ -55,39 +55,41 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
ref: ${{ github.ref }} ref: ${{ github.ref }}
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 9.x dotnet-version: 10.x
- name: Build
run: dotnet build src
- name: Publish for mac-os - name: Publish for mac-os
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r osx-x64 --self-contained -o dist/mac-os run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r osx-x64 --self-contained -o dist/mac-os
- name: Rename mac-os binary - name: Create zip for mac-os
run: mv dist/mac-os/BGR.Console dist/bgr_osx_x64 run: zip -j dist/bgr_osx_x64.zip dist/mac-os/*
- name: Publish for linux-os - name: Publish for linux-os
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r linux-x64 --self-contained -o dist/linux-os run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r linux-x64 --self-contained -o dist/linux-os
- name: Rename linux-os binary - name: Create zip for linux-os
run: mv dist/linux-os/BGR.Console dist/bgr_linux_x64 run: zip -j dist/bgr_linux_x64.zip dist/linux-os/*
- name: Publish for windows-os - name: Publish for windows-os
run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r win-x64 --self-contained -o dist/windows-os run: dotnet publish src/BGR.Console/BGR.Console.csproj -c Release -r win-x64 --self-contained -o dist/windows-os
- name: Rename windows-os binary - name: Create zip for windows-os
run: mv dist/windows-os/BGR.Console.exe dist/bgr_windows_x64.exe run: zip -j dist/bgr_windows_x64.zip dist/windows-os/*
- name: Get project version - name: Get project version
uses: kzrnm/get-net-sdk-project-versions-action@v1 uses: kzrnm/get-net-sdk-project-versions-action@v1
id: get-version id: get-version
with: with:
proj-path: src/BGR.Console/BGR.Console.csproj proj-path: src/BGR.Console/BGR.Console.csproj
- name: Download changlog - name: Download changlog
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: change-log name: change-log
path: src/BGR.Console path: src/BGR.Console
- name: Create release - name: Create release
uses: softprops/action-gh-release@v1 uses: softprops/action-gh-release@v3
with: with:
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
name: bgr v${{ steps.get-version.outputs.version }} name: bgr v${{ steps.get-version.outputs.version }}
@@ -95,6 +97,6 @@ jobs:
draft: false draft: false
body_path: src/BGR.Console/CHANGELOG.md body_path: src/BGR.Console/CHANGELOG.md
files: | files: |
dist/bgr_osx_x64 dist/bgr_osx_x64.zip
dist/bgr_linux_x64 dist/bgr_linux_x64.zip
dist/bgr_windows_x64.exe dist/bgr_windows_x64.zip
+14 -14
View File
@@ -18,14 +18,14 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 9.x.x dotnet-version: 10.x.x
- name: Format project - name: Format project
run: dotnet format src --verbosity normal run: dotnet format src --verbosity normal
- name: Commit Changes - name: Commit Changes
@@ -49,14 +49,14 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
- name: Setup .NET 9 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 9.x dotnet-version: 10.x
- name: Install report generator - name: Install report generator
run: dotnet tool install --global dotnet-reportgenerator-globaltool run: dotnet tool install --global dotnet-reportgenerator-globaltool
- name: Restore dependencies - name: Restore dependencies
@@ -68,7 +68,7 @@ jobs:
- name: Rename test coverage report - 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 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 }} - name: Upload test coverage report for ${{ matrix.os }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: test-coverage-${{ matrix.os }} name: test-coverage-${{ matrix.os }}
path: src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml path: src/BGR.Console.Tests/TestResults/Coverage/${{ matrix.os }}-coverage.cobertura.xml
@@ -78,26 +78,26 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
token: ${{ secrets.ACTIONS_PAT }} token: ${{ secrets.ACTIONS_PAT }}
- name: Download ubuntu-latest report - name: Download ubuntu-latest report
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: test-coverage-ubuntu-latest name: test-coverage-ubuntu-latest
path: ./coverage path: ./coverage
- name: Download windows-latest report - name: Download windows-latest report
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: test-coverage-windows-latest name: test-coverage-windows-latest
path: ./coverage path: ./coverage
- name: Download macos-latest report - name: Download macos-latest report
uses: actions/download-artifact@v4 uses: actions/download-artifact@v8
with: with:
name: test-coverage-macos-latest name: test-coverage-macos-latest
path: ./coverage path: ./coverage
- name: Upload test coverage reports - name: Upload test coverage reports
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v7
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
+25 -2
View File
@@ -1,13 +1,21 @@
[![codecov](https://codecov.io/gh/StevanFreeborn/bgr/graph/badge.svg?token=5qp2LpvOZA)](https://codecov.io/gh/StevanFreeborn/bgr)
[![Pull Request](https://github.com/StevanFreeborn/bgr/actions/workflows/pull_request.yml/badge.svg)](https://github.com/StevanFreeborn/bgr/actions/workflows/pull_request.yml)
[![Publish](https://github.com/StevanFreeborn/bgr/actions/workflows/publish.yml/badge.svg)](https://github.com/StevanFreeborn/bgr/actions/workflows/publish.yml)
![GitHub Release](https://img.shields.io/github/v/release/StevanFreeborn/bgr?display_name=tag)
![GitHub License](https://img.shields.io/github/license/StevanFreeborn/bgr)
[![semantic-release: angular](https://img.shields.io/badge/semantic--release-angular-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release)
![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/StevanFreeborn/bgr/total)
# BGR # BGR
This is a background removal app that uses machine learning models to remove the background from an image. The app is available for download as a single file executable [here](). The app can also be built from source using the [.NET SDK](https://dotnet.microsoft.com/download). If you'd like to read more about the idea and initial development behind the app you can read the blog post I wrote about it [here](). This is a background removal app that uses machine learning models to remove the background from an image. The app is available for download [here](https://github.com/StevanFreeborn/bgr/releases). You'll just need to unzip the files, place them in a directory, and add it to your PATH. You can rename the executable to your preferred name. The app can also be built from source using the [.NET SDK](https://dotnet.microsoft.com/download). If you'd like to read more about the idea and initial development behind the app you can read the blog post I wrote about it [here](https://blog.stevanfreeborn.com/building-a-background-removal-app-with-machine-learning-and-dotnet).
## Usage ## Usage
The app is meant to be run from the command line. For example: The app is meant to be run from the command line. For example:
```pwsh ```pwsh
bgr /path/to/image.jpg --output /path/to/output.jpg bgr /path/to/image.jpg --output /path/to/output.png
``` ```
You can find the full list of commands, arguments, and options using the `--help` option: You can find the full list of commands, arguments, and options using the `--help` option:
@@ -26,6 +34,21 @@ bgr --help
![Output Image](examples/output.png) ![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 ## 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. 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.
+5
View File
@@ -0,0 +1,5 @@
{
"sdk": {
"rollForward": "latestFeature"
}
}
@@ -2,25 +2,26 @@
<PropertyGroup> <PropertyGroup>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<NoWarn>1591;</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </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> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Moq" Version="4.20.72" /> <PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="Shouldly" Version="4.3.0" /> <PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
<PackageReference Include="Spectre.Console.Testing" Version="0.49.1" /> <PackageReference Include="Spectre.Console.Testing" Version="0.57.2" />
<PackageReference Include="xunit" Version="2.9.3" /> <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> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -183,7 +183,10 @@ public class ImageSharpProcessorTests : IDisposable
await mask.SaveAsPngAsync(maskStream); await mask.SaveAsPngAsync(maskStream);
maskStream.Position = 0; 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.ShouldNotBeNull();
result.Length.ShouldBeGreaterThan(0); result.Length.ShouldBeGreaterThan(0);
@@ -203,6 +206,37 @@ public class ImageSharpProcessorTests : IDisposable
resultImage[1, 1].A.ShouldBe((byte)255); 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] [Fact]
public async Task SaveImageAsync_WhenCalled_ItShouldSaveImageToDiskAtProvidedPath() public async Task SaveImageAsync_WhenCalled_ItShouldSaveImageToDiskAtProvidedPath()
{ {
@@ -1,5 +1,3 @@
using BGR.Console.Removal.Onnx;
using Microsoft.ML.OnnxRuntime.Tensors; using Microsoft.ML.OnnxRuntime.Tensors;
namespace BGR.Console.Tests.Unit; namespace BGR.Console.Tests.Unit;
@@ -1,6 +1,4 @@
using Microsoft.Extensions.Logging; using System.Diagnostics.CodeAnalysis;
using Spectre.Console.Cli;
namespace BGR.Console.Tests.Unit; namespace BGR.Console.Tests.Unit;
@@ -28,6 +26,7 @@ public class RemovalCommandTests : IDisposable
[Theory] [Theory]
[InlineData("output.png", false)] [InlineData("output.png", false)]
[InlineData("", true)] [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) public async Task ExecuteAsync_WhenCalled_ItShouldProcessImage(string outputPath, bool includeMask)
{ {
var imagePath = $"{Guid.NewGuid()}.png"; var imagePath = $"{Guid.NewGuid()}.png";
@@ -73,7 +72,7 @@ public class RemovalCommandTests : IDisposable
.ReturnsAsync(maskStream); .ReturnsAsync(maskStream);
_imageProcessorMock _imageProcessorMock
.Setup(p => p.RemoveBackgroundAsync(image.Data, maskStream)) .Setup(p => p.RemoveBackgroundAsync(image.Data, maskStream, It.IsAny<byte>(), It.IsAny<byte>()))
.ReturnsAsync(outputStream); .ReturnsAsync(outputStream);
var commandContext = new CommandContext( var commandContext = new CommandContext(
@@ -92,7 +91,7 @@ public class RemovalCommandTests : IDisposable
_imageProcessorMock.Verify(p => p.CreateTensorInputAsync(image.Data, model), Times.Once); _imageProcessorMock.Verify(p => p.CreateTensorInputAsync(image.Data, model), Times.Once);
_inferenceRunnerMock.Verify(r => r.Run(model.Bytes, inputTensor), 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.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); _imageProcessorMock.Verify(p => p.SaveImageAsync(outputStream, It.IsAny<string>()), Times.AtLeastOnce);
File.Delete(imagePath); File.Delete(imagePath);
+10 -10
View File
@@ -38,17 +38,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.10" />
<PackageReference Include="Microsoft.Extensions.Options" Version="9.0.1" /> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" /> <PackageReference Include="Microsoft.ML.OnnxRuntime" Version="1.27.1" />
<PackageReference Include="Serilog" Version="4.2.0" /> <PackageReference Include="Serilog" Version="4.4.0" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="9.0.0" /> <PackageReference Include="Serilog.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" /> <PackageReference Include="Serilog.Formatting.Compact" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.6" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Spectre.Console" Version="0.49.1" /> <PackageReference Include="Spectre.Console" Version="0.57.2" />
<PackageReference Include="Spectre.Console.Cli" Version="0.49.1" /> <PackageReference Include="Spectre.Console.Cli" Version="0.55.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+4 -5
View File
@@ -2,14 +2,13 @@
All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines. All notable changes to this project will be documented in this file. See [versionize](https://github.com/versionize/versionize) for commit guidelines.
<a name="0.0.0"></a> <a name="0.1.0"></a>
## [0.0.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.0.0) (2025-02-14) ## [0.1.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.1.0) (2026-07-15)
### Features ### Features
* initial working POC ([651869c](https://www.github.com/StevanFreeborn/bgr/commit/651869c27ff9ed390d78bf7ae6b81b3aa1acefb3)) * improve background removal feathering ([e099144](https://www.github.com/StevanFreeborn/bgr/commit/e099144127ae719f85f3375be8be72fe3b5914c6))
* refactor to proper app + write tests ([42435ad](https://www.github.com/StevanFreeborn/bgr/commit/42435ad9150db459ee97a399c53050a7ec58012c)) * upgrade to .NET 10 ([45dc9de](https://www.github.com/StevanFreeborn/bgr/commit/45dc9de2232e64b234b8016e64e7b281b613e6f9))
* working on adding abstractions ([baf4035](https://www.github.com/StevanFreeborn/bgr/commit/baf40356164be64735ed53fa42966d664a41a3a9))
<a name="0.0.0"></a> <a name="0.0.0"></a>
## [0.0.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.0.0) (2025-02-14) ## [0.0.0](https://www.github.com/StevanFreeborn/bgr/releases/tag/v0.0.0) (2025-02-14)
+1 -1
View File
@@ -8,7 +8,7 @@ internal abstract class ImageProcessor
public abstract Task<Stream> GenerateMaskAsync(ITensor<float> maskTensor, int width, int height); 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); public abstract Task SaveImageAsync(Stream image, string path);
@@ -67,7 +67,7 @@ internal class ImageSharpProcessor : ImageProcessor
return stream; 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; image.Position = 0;
mask.Position = 0; mask.Position = 0;
@@ -76,19 +76,14 @@ internal class ImageSharpProcessor : ImageProcessor
var maskImage = await Image.LoadAsync<Rgba32>(mask); var maskImage = await Image.LoadAsync<Rgba32>(mask);
using var imageWithBgRemoved = new Image<Rgba32>(imageWithBg.Width, imageWithBg.Height); 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) => WalkImage(imageWithBg.Height, imageWithBg.Width, (x, y) =>
{ {
var sourcePixel = imageWithBg[x, y]; var sourcePixel = imageWithBg[x, y];
var maskPixel = maskImage[x, y]; var maskPixel = maskImage[x, y];
var alpha = maskPixel.R; var alpha = AdjustAlpha(maskPixel.R, featherMin, featherMax);
imageWithBgRemoved[x, y] = alpha > alphaThreshold imageWithBgRemoved[x, y] = new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, alpha);
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
: transparentPixel;
}); });
var result = new MemoryStream(); var result = new MemoryStream();
@@ -107,12 +102,7 @@ internal class ImageSharpProcessor : ImageProcessor
private static float Normalize(float value) private static float Normalize(float value)
{ {
const float binarizationThreshold = 0.5f; return value * value;
const float normalizationFactor = 2f;
return value > binarizationThreshold
? (value - binarizationThreshold) * normalizationFactor
: 0f;
} }
private static byte ConvertToGreyscale(float value) private static byte ConvertToGreyscale(float value)
@@ -129,4 +119,21 @@ internal class ImageSharpProcessor : ImageProcessor
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * 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);
}
} }
+1 -1
View File
@@ -1,6 +1,6 @@
namespace BGR.Console.Removal.Onnx; namespace BGR.Console.Removal.Onnx;
public class OnnxTensor : ITensor<float> internal class OnnxTensor : ITensor<float>
{ {
private readonly Tensor<float> _tensor; private readonly Tensor<float> _tensor;
+22 -4
View File
@@ -1,5 +1,3 @@
using System.Diagnostics;
namespace BGR.Console.Removal; namespace BGR.Console.Removal;
internal class RemovalCommand( internal class RemovalCommand(
@@ -16,7 +14,19 @@ internal class RemovalCommand(
private readonly IAnsiConsole _console = console; private readonly IAnsiConsole _console = console;
private readonly ILogger<RemovalCommand> _logger = logger; private readonly ILogger<RemovalCommand> _logger = logger;
public override async Task<int> ExecuteAsync(CommandContext context, Settings settings) public async Task<int> ExecuteAsync(
CommandContext context,
Settings settings
)
{
return await ExecuteAsync(context, settings, CancellationToken.None);
}
protected override async Task<int> ExecuteAsync(
CommandContext context,
Settings settings,
CancellationToken cancellationToken
)
{ {
await _console.Status() await _console.Status()
.Spinner(Spinner.Known.Dots) .Spinner(Spinner.Known.Dots)
@@ -56,7 +66,7 @@ internal class RemovalCommand(
ctx.Status("Removing background..."); ctx.Status("Removing background...");
var output = await _logger.TimeAndLogActionAsync( var output = await _logger.TimeAndLogActionAsync(
"Removing background", "Removing background",
async () => await _imageProcessor.RemoveBackgroundAsync(image.Data, mask) async () => await _imageProcessor.RemoveBackgroundAsync(image.Data, mask, settings.FeatherMin, settings.FeatherMax)
); );
if (settings.IncludeMask) if (settings.IncludeMask)
@@ -105,6 +115,14 @@ internal class RemovalCommand(
[Description("Path to output image without background to. File extension will always be .png")] [Description("Path to output image without background to. File extension will always be .png")]
public string Output { get; init; } = string.Empty; public string Output { get; init; } = string.Empty;
[CommandOption("--feather-min")]
[Description("Minimum mask value below which pixels become fully transparent (default: 70)")]
public byte FeatherMin { get; init; } = 70;
[CommandOption("--feather-max")]
[Description("Maximum mask value above which pixels become fully opaque (default: 117)")]
public byte FeatherMax { get; init; } = 117;
public string ResourceName => Models[Model]; public string ResourceName => Models[Model];
public string MaskPath => GetOutputPath("_mask"); public string MaskPath => GetOutputPath("_mask");
+2 -1
View File
@@ -1,6 +1,6 @@
<Project> <Project>
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AnalysisLevel>latest</AnalysisLevel> <AnalysisLevel>latest</AnalysisLevel>
@@ -8,6 +8,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors> <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup> </PropertyGroup>
</Project> </Project>