feat: working on adding abstractions

This commit is contained in:
Stevan Freeborn
2025-02-09 23:22:21 -06:00
parent 651869c27f
commit baf4035616
34 changed files with 1122 additions and 37 deletions
@@ -0,0 +1,47 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4">
<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="Moq" Version="4.20.72" />
<PackageReference Include="Shouldly" Version="4.3.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.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>
</ItemGroup>
<PropertyGroup>
<CollectCoverage>true</CollectCoverage>
<CoverletOutput>./TestResults/coverage/</CoverletOutput>
<CoverletOutputFormat>cobertura</CoverletOutputFormat>
<Include>[BGR.Console]*</Include>
<ExcludeByFile>**/Program.cs</ExcludeByFile>
</PropertyGroup>
<Target Name="GenerateHtmlCoverageReport" AfterTargets="GenerateCoverageResultAfterTest">
<Exec Command="reportgenerator -reports:./TestResults/coverage/*.xml -targetdir:./TestResults/coverage/report/ -reporttypes:Html_Dark" />
</Target>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="Shouldly" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BGR.Console\BGR.Console.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,26 @@
namespace BGR.Console.Tests.Unit;
public class ResourceManagerTests
{
private readonly ResourceManager _resourceManager = new();
[Fact]
public void GetResource_WhenResourceExists_ItShouldReturnStream()
{
var resourceName = "u2net.onnx";
using var stream = _resourceManager.GetResource(resourceName);
stream.ShouldNotBeNull();
}
[Fact]
public void GetResource_WhenResourceDoesNotExist_ItShouldThrowException()
{
var resourceName = "nonexistent.onnx";
var act = () => _resourceManager.GetResource(resourceName);
act.ShouldThrow<FileNotFoundException>();
}
}
@@ -0,0 +1,84 @@
namespace BGR.Console.Tests.Unit;
public class TypeRegistrarTests
{
[Fact]
public void Constructor_WhenCalled_ItShouldNotThrowShould()
{
var mockBuilder = new Mock<IHostBuilder>();
Should.NotThrow(() => new TypeRegistrar(mockBuilder.Object));
}
[Fact]
public void Build_WhenCalled_ItShouldReturnResolverAndBuildHost()
{
var mockHost = new Mock<IHost>();
var mockBuilder = new Mock<IHostBuilder>();
mockBuilder
.Setup(static b => b.Build())
.Returns(mockHost.Object);
var registrar = new TypeRegistrar(mockBuilder.Object);
var resolver = registrar.Build();
resolver.ShouldBeOfType<TypeResolver>();
mockBuilder.Verify(static b => b.Build(), Times.Once);
}
[Fact]
public void Register_WhenCalledWithType_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
registrar.Register(typeof(IService), typeof(ServiceImplementation));
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldNotBeNull();
service.ShouldBeOfType<ServiceImplementation>();
}
[Fact]
public void RegisterInstance_WhenCalledWithInstance_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
var instance = new ServiceImplementation();
registrar.RegisterInstance(typeof(IService), instance);
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldBeSameAs(instance);
}
[Fact]
public void RegisterLazy_WhenCalledWithFunc_ItShouldAddToContainer()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
registrar.RegisterLazy(typeof(IService), static () => new ServiceImplementation());
using var host = builder.Build();
var service = host.Services.GetService<IService>();
service.ShouldNotBeNull();
service.ShouldBeOfType<ServiceImplementation>();
}
[Fact]
public void RegisterLazy_WhenFuncIsNull_ItShouldThrow()
{
var builder = Host.CreateDefaultBuilder();
var registrar = new TypeRegistrar(builder);
Should.Throw<ArgumentNullException>(() => registrar.RegisterLazy(typeof(IService), null!));
}
private interface IService { }
private sealed class ServiceImplementation : IService { }
}
@@ -0,0 +1,71 @@
namespace BGR.Console.Tests.Unit;
public class TypeResolverTests
{
[Fact]
public void Constructor_WhenCalledWithNullHost_ItShouldThrowArgumentNullException()
{
Should.Throw<ArgumentNullException>(static () => new TypeResolver(null!));
}
[Fact]
public void Resolve_WhenTypeIsNull_ItShouldReturnNull()
{
var mockHost = new Mock<IHost>();
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(null);
result.ShouldBeNull();
}
[Fact]
public void Resolve_WhenCalledWithRegisteredType_ItShouldReturnAnInstance()
{
var services = new ServiceCollection();
services.AddSingleton(new TestService());
var mockHost = new Mock<IHost>();
mockHost
.Setup(static h => h.Services)
.Returns(services.BuildServiceProvider());
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(typeof(TestService));
result.ShouldNotBeNull();
result.ShouldBeOfType<TestService>();
}
[Fact]
public void Resolve_WhenCalledWithUnregisteredType_ItShouldReturnNull()
{
var services = new ServiceCollection();
var mockHost = new Mock<IHost>();
mockHost
.Setup(static h => h.Services)
.Returns(services.BuildServiceProvider());
using var resolver = new TypeResolver(mockHost.Object);
var result = resolver.Resolve(typeof(TestService));
result.ShouldBeNull();
}
[Fact]
public void Dispose_WhenCalled_ItShouldAlsoDisposeHost()
{
var mockHost = new Mock<IHost>();
var resolver = new TypeResolver(mockHost.Object);
resolver.Dispose();
mockHost.Verify(static h => h.Dispose(), Times.Once);
}
private sealed class TestService { }
}
+7
View File
@@ -0,0 +1,7 @@
global using BGR.Console.Common;
global using BGR.Console.Resources;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Moq;
+29
View File
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<InternalsVisibleTo Include="$(AssemblyName).Tests" />
</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="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" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\Files\**\*" />
</ItemGroup>
</Project>
@@ -0,0 +1,12 @@
namespace BGR.Console.Common;
internal static class HostBuilderExtensions
{
public static CommandApp BuildApp(this IHostBuilder builder)
{
var registrar = new TypeRegistrar(builder);
var app = new CommandApp(registrar);
return app;
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace BGR.Console.Common;
internal class TypeRegistrar(IHostBuilder builder) : ITypeRegistrar
{
private readonly IHostBuilder _builder = builder;
public ITypeResolver Build()
{
return new TypeResolver(_builder.Build());
}
public void Register(Type service, Type implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterInstance(Type service, object implementation)
{
_builder.ConfigureServices((_, services) => services.AddSingleton(service, implementation));
}
public void RegisterLazy(Type service, Func<object> func)
{
ArgumentNullException.ThrowIfNull(func);
_builder.ConfigureServices((_, services) => services.AddSingleton(service, _ => func()));
}
}
+16
View File
@@ -0,0 +1,16 @@
namespace BGR.Console.Common;
internal class TypeResolver(IHost provider) : ITypeResolver, IDisposable
{
private readonly IHost _host = provider ?? throw new ArgumentNullException(nameof(provider));
public object? Resolve(Type? type)
{
return type is not null ? _host.Services.GetService(type) : null;
}
public void Dispose()
{
_host.Dispose();
}
}
+234
View File
@@ -0,0 +1,234 @@
Log.Logger = new LoggerConfiguration()
.WriteTo.File(
formatter: new CompactJsonFormatter(),
path: Path.Combine(AppContext.BaseDirectory, "logs", "log.jsonl"),
rollingInterval: RollingInterval.Day
)
.Enrich.FromLogContext()
.MinimumLevel.Verbose()
.MinimumLevel.Override("Microsoft", LogEventLevel.Fatal)
.CreateLogger();
try
{
var appName = Assembly.GetExecutingAssembly().GetName().Name;
Log.Information("Starting {AppName}", appName);
await Host.CreateDefaultBuilder()
.ConfigureLogging(static logging => logging.ClearProviders())
.ConfigureServices(static (_, services) =>
{
services.AddSerilog();
services.AddSingleton<IResourceManager, ResourceManager>();
})
.BuildApp()
.RunAsync(args);
Log.Information("Stopping {AppName}", appName);
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
throw;
}
finally
{
await Log.CloseAndFlushAsync();
}
if (args.Length < 1)
{
Console.WriteLine("Usage: BackgroundRemover <input_image_path>");
return;
}
var inputImagePath = args[0];
var maskImagePath = Path.ChangeExtension(inputImagePath, null) + "_mask.png";
var outputImagePath = Path.ChangeExtension(inputImagePath, null) + "_no_bg.png";
try
{
var assembly = Assembly.GetExecutingAssembly();
var resourceName = "BGR.Console.Resources.Files.rmbg.onnx";
using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new FileNotFoundException("Model not found in embedded resources.");
var modelBytes = new byte[stream.Length];
stream.ReadExactly(modelBytes);
using var image = await Image.LoadAsync<Rgba32>(inputImagePath);
var inputTensor = CreateTensorInput(image);
using var options = new SessionOptions() { LogSeverityLevel = OrtLoggingLevel.ORT_LOGGING_LEVEL_ERROR };
using InferenceSession session = new(modelBytes, options);
var inputs = new List<NamedOnnxValue>()
{
NamedOnnxValue.CreateFromTensor(session.InputNames[0], inputTensor),
};
using var results = session.Run(inputs);
var outputTensor = results[0].AsTensor<float>();
using var mask = GenerateMask(outputTensor, image.Width, image.Height);
using var bgRemoved = GetImageWithBackgroundRemoved(image, mask);
var encoder = new PngEncoder { CompressionLevel = PngCompressionLevel.BestCompression };
await mask.SaveAsync(maskImagePath, encoder);
await bgRemoved.SaveAsync(outputImagePath, encoder);
Console.WriteLine($"Background removed and saved to {outputImagePath}");
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
throw;
}
static Tensor<float> CreateTensorInput(Image<Rgba32> image)
{
// U2Net expects input images to be 320x320. This is dependent on the model.
const int targetWidth = 1024;
const int targetHeight = 1024;
// ImageNet normalization parameters
// source:
// - https://www.image-net.org/
// - https://pytorch.org calculated these values from the ImageNet dataset
// and they are commonly used for models trained on ImageNet so we use them here
// to normalize the input image to better match the distribution of the data the model was trained on
// NOTE: These values are not universal and may vary for different models
const float rMean = 0.485f; // Mean value for Red channel
const float gMean = 0.456f; // Mean value for Green channel
const float bMean = 0.406f; // Mean value for Blue channel
const float rStd = 0.229f; // Standard deviation for Red channel
const float gStd = 0.224f; // Standard deviation for Green channel
const float bStd = 0.225f; // Standard deviation for Blue channel
const float pixelMax = 255f; // Maximum pixel intensity for normalization
// Create a temporary image for preprocessing
using var resized = image.Clone();
resized.Mutate(x => x.Resize(targetWidth, targetHeight));
// Create tensor of shape (1, 3, 320, 320)
// 1 for batch size, 3 for RGB channels, 320x320 for image dimensions
DenseTensor<float> tensor = new([1, 3, targetHeight, targetWidth]);
// Normalize pixel values and copy to tensor
WalkImage(resized.Height, resized.Width, (x, y) =>
{
var pixel = resized[x, y];
// u2net expects expect input images to be normalized using ImageNet mean and std
// to better match the distribution of the data the model was trained on
// Normalize to range [0, 1] and standardize using ImageNet mean/std
// The tensor is filled with normalized pixel values
tensor[0, 0, y, x] = ((pixel.R / pixelMax) - rMean) / rStd; // Red channel
tensor[0, 1, y, x] = ((pixel.G / pixelMax) - gMean) / gStd; // Green channel
tensor[0, 2, y, x] = ((pixel.B / pixelMax) - bMean) / bStd; // Blue channel
});
return tensor;
}
static Image<Rgba32> GenerateMask(Tensor<float> maskTensor, int width, int height)
{
var mask = new Image<Rgba32>(width, height);
var sourceHeight = maskTensor.Dimensions[2]; // Height of the original tensor mask
var sourceWidth = maskTensor.Dimensions[3]; // Width of the original tensor mask
using Image<Rgba32> tempMask = new(sourceWidth, sourceHeight);
// Sigmoid function parameters
const float sigmoidScale = 1f; // Scaling factor for sigmoid activation
const float sigmoidShift = 1f; // Shift factor in the denominator of the sigmoid function
const float sigmoidDivisor = -1f; // Multiplier for the exponent in the sigmoid function
static float CalculateSigmoid(float x)
{
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
}
const float binarizationThreshold = 0.5f; // Threshold to determine foreground vs. background
const float normalizationFactor = 2f; // Scales the thresholded value to enhance contrast
// Pixel intensity values
const byte maxIntensity = 255; // Maximum grayscale intensity
const byte opaqueAlpha = 255; // Fully opaque alpha value
WalkImage(sourceHeight, sourceWidth, (x, y) =>
{
// a sigmoid function is a function that produces an S-shaped curve
// it is often used in machine learning and statistics to model probabilities
// the sigmoid function is defined as:
// f(x) = 1 / (1 + e^(-x))
// where e is the base of the natural logarithm and x is the input value
// the raw tensor values for our mask are going to be real unbounded numbers
// i.e. -1.5, 0.5, 2.0, etc.
// the sigmoid function will map these values to a range between 0 and 1
// this allows us to say that value closer to 0 is background and value
// closer to 1 is foreground
var sigmoidValue = CalculateSigmoid(maskTensor[0, 0, y, x]);
// now we want to threshold the sigmoid value to determine if it is foreground or background
// we are arbitrarily choosing 0.5 as the threshold. so if the sigmoid value is greater than
// 0.5 we will consider it foreground and if it is less than 0.5 we will consider it background
// when a sigmoid value is greater than 0.5 we will subtract the threshold from it
// and multiply it by 2 this way the intensity value will be larger for values closer to 1
// and create more contrast in the mask
var normalizedValue = sigmoidValue > binarizationThreshold
? (sigmoidValue - binarizationThreshold) * normalizationFactor
: 0f;
// Convert to an 8-bit grayscale intensity
var intensity = (byte)(normalizedValue * maxIntensity);
// Store the pixel with full opacity
tempMask[x, y] = new Rgba32(intensity, intensity, intensity, opaqueAlpha);
});
// Resize the mask to match the target dimensions
tempMask.Mutate(x => x.Resize(width, height));
// Copy the resized mask to the final output image
WalkImage(height, width, (x, y) => mask[x, y] = tempMask[x, y]);
return mask;
}
static Image<Rgba32> GetImageWithBackgroundRemoved(Image<Rgba32> image, Image<Rgba32> mask)
{
Image<Rgba32> result = new(image.Width, image.Height);
const byte alphaThreshold = 20;
Rgba32 transparentPixel = new(0, 0, 0, 0);
WalkImage(image.Height, image.Width, (x, y) =>
{
var sourcePixel = image[x, y];
var maskPixel = mask[x, y];
var alpha = maskPixel.R;
result[x, y] = alpha > alphaThreshold
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
: transparentPixel;
});
return result;
}
static void WalkImage(int height, int width, Action<int, int> action)
{
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
action(x, y);
}
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace BGR.Console.Removal;
internal interface IImage
{
int Width { get; }
int Height { get; }
void Resize(int width, int height);
IPixel GetPixel(int x, int y);
}
+8
View File
@@ -0,0 +1,8 @@
namespace BGR.Console.Removal;
internal interface IPixel
{
float R { get; }
float G { get; }
float B { get; }
}
+9
View File
@@ -0,0 +1,9 @@
namespace BGR.Console.Removal;
internal interface ITensor<T>
{
int Height { get; }
int Width { get; }
void SetValue(int batch, int channel, int y, int x, T value);
float GetValue(int batch, int channel, int y, int x);
}
+22
View File
@@ -0,0 +1,22 @@
namespace BGR.Console.Removal;
internal abstract class ImageProcessor
{
public abstract Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model);
public abstract Task<Stream> GenerateMaskAsync(OnnxTensor maskTensor, int width, int height);
public abstract Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask);
protected static void WalkImage(int height, int width, Action<int, int> action)
{
for (var y = 0; y < height; y++)
{
for (var x = 0; x < width; x++)
{
action(x, y);
}
}
}
}
@@ -0,0 +1,102 @@
namespace BGR.Console.Removal.ImageSharp;
internal class ImageSharpProcessor : ImageProcessor
{
public override async Task<ITensor<float>> CreateTensorInputAsync(Stream image, Model model)
{
using var resized = await Image.LoadAsync<Rgba32>(image);
resized.Mutate(x => x.Resize(model.InputWidth, model.InputHeight));
const int batchSize = 1;
const int channels = 3;
var tensor = new OnnxTensor(batchSize, channels, model.InputHeight, model.InputWidth);
WalkImage(resized.Height, resized.Width, (x, y) =>
{
var pixel = resized[x, y];
tensor.SetValue(0, 0, y, x, model.NormalizeRed(pixel.R));
tensor.SetValue(0, 1, y, x, model.NormalizeGreen(pixel.G));
tensor.SetValue(0, 2, y, x, model.NormalizeBlue(pixel.B));
});
return tensor;
}
public override async Task<Stream> GenerateMaskAsync(OnnxTensor maskTensor, int width, int height)
{
using var mask = new Image<Rgba32>(width, height);
using Image<Rgba32> tempMask = new(maskTensor.Width, maskTensor.Height);
const byte opaqueAlpha = 255;
WalkImage(maskTensor.Height, maskTensor.Width, (x, y) =>
{
var sigmoidValue = CalculateSigmoid(maskTensor.GetValue(0, 0, y, x));
var normalizedValue = Normalize(sigmoidValue);
var intensity = ConvertToGreyscale(normalizedValue);
tempMask[x, y] = new Rgba32(intensity, intensity, intensity, opaqueAlpha);
});
tempMask.Mutate(x => x.Resize(width, height));
WalkImage(height, width, (x, y) => mask[x, y] = tempMask[x, y]);
var stream = new MemoryStream();
await mask.SaveAsync(stream, new PngEncoder());
return stream;
}
public override async Task<Stream> RemoveBackgroundAsync(Stream image, Stream mask)
{
var imageWithBg = await Image.LoadAsync<Rgba32>(image);
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;
imageWithBgRemoved[x, y] = alpha > alphaThreshold
? new Rgba32(sourcePixel.R, sourcePixel.G, sourcePixel.B, sourcePixel.A)
: transparentPixel;
});
var result = new MemoryStream();
await imageWithBgRemoved.SaveAsync(result, new PngEncoder());
return result;
}
private static float Normalize(float value)
{
const float binarizationThreshold = 0.5f;
const float normalizationFactor = 2f;
return value > binarizationThreshold
? (value - binarizationThreshold) * normalizationFactor
: 0f;
}
private static byte ConvertToGreyscale(float value)
{
const float maxIntensity = 255f;
return (byte)(value * maxIntensity);
}
private static float CalculateSigmoid(float x)
{
const float sigmoidScale = 1f;
const float sigmoidShift = 1f;
const float sigmoidDivisor = -1f;
return sigmoidScale / (sigmoidShift + MathF.Exp(sigmoidDivisor * x));
}
}
@@ -0,0 +1,19 @@
namespace BGR.Console.Removal.ImageSharp;
internal class SharpImage(Image<Rgba32> image) : IImage
{
private readonly Image<Rgba32> _image = image;
public int Width => _image.Width;
public int Height => _image.Height;
public void Resize(int width, int height)
{
_image.Mutate(x => x.Resize(width, height));
}
public IPixel GetPixel(int x, int y)
{
return new SharpPixel(_image[x, y]);
}
}
@@ -0,0 +1,10 @@
namespace BGR.Console.Removal.ImageSharp;
internal class SharpPixel(Rgba32 pixel) : IPixel
{
private readonly Rgba32 _pixel = pixel;
public float R => _pixel.R;
public float G => _pixel.G;
public float B => _pixel.B;
}
@@ -0,0 +1,13 @@
namespace BGR.Console.Removal.Models;
internal class ModNetModel : Model
{
public override int InputWidth => 512;
public override int InputHeight => 512;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
+34
View File
@@ -0,0 +1,34 @@
namespace BGR.Console.Removal.Models;
internal abstract class Model
{
private const float PixelMax = 255f;
public abstract int InputWidth { get; }
public abstract int InputHeight { get; }
public abstract float RedNormalizationMean { get; }
public abstract float GreenNormalizationMean { get; }
public abstract float BlueNormalizationMean { get; }
public abstract float RedNormalizationStd { get; }
public abstract float GreenNormalizationStd { get; }
public abstract float BlueNormalizationStd { get; }
public float NormalizeRed(float value)
{
return Normalize(value, RedNormalizationMean, RedNormalizationStd);
}
public float NormalizeGreen(float value)
{
return Normalize(value, GreenNormalizationMean, GreenNormalizationStd);
}
public float NormalizeBlue(float value)
{
return Normalize(value, BlueNormalizationMean, BlueNormalizationStd);
}
private static float Normalize(float value, float mean, float std)
{
return ((value / PixelMax) - mean) / std;
}
}
@@ -0,0 +1,14 @@
namespace BGR.Console.Removal.Models;
internal class RmbgModel : Model
{
public override int InputWidth => 1024;
public override int InputHeight => 1024;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
@@ -0,0 +1,13 @@
namespace BGR.Console.Removal.Models;
internal class U2NetModel : Model
{
public override int InputWidth => 320;
public override int InputHeight => 320;
public override float RedNormalizationMean => 0.485f;
public override float GreenNormalizationMean => 0.456f;
public override float BlueNormalizationMean => 0.406f;
public override float RedNormalizationStd => 0.229f;
public override float GreenNormalizationStd => 0.224f;
public override float BlueNormalizationStd => 0.225f;
}
@@ -0,0 +1,37 @@
namespace BGR.Console.Removal.Onnx;
public class OnnxTensor(
int batchSize,
int channels,
int height,
int width
) : ITensor<float>
{
private readonly DenseTensor<float> _tensor =
new([batchSize, channels, height, width]);
public int Height => _tensor.Dimensions[2];
public int Width => _tensor.Dimensions[3];
public void SetValue(
int batch,
int channel,
int y,
int x,
float value
)
{
_tensor[batch, channel, y, x] = value;
}
public float GetValue(
int batch,
int channel,
int y,
int x
)
{
return _tensor[batch, channel, y, x];
}
}
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:07c308cf0fc7e6e8b2065a12ed7fc07e1de8febb7dc7839d7b7f15dd66584df9
size 25888640
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fcea23951a378f92634834888896cc1eec54655366ae6e949282646ce17c5420
size 366087549
@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8d10d2f3bb75ae3b6d527c77944fc5e7dcd94b29809d47a739a7a728a912b491
size 175997641
@@ -0,0 +1,6 @@
namespace BGR.Console.Resources;
internal interface IResourceManager
{
Stream GetResource(string resourceName);
}
@@ -0,0 +1,14 @@
namespace BGR.Console.Resources;
internal sealed class ResourceManager : IResourceManager
{
public Stream GetResource(string resourceName)
{
var name = $"{nameof(BGR)}.{nameof(Console)}.{nameof(Resources)}.Files.{resourceName}";
var assembly = Assembly.GetExecutingAssembly();
var names = assembly.GetManifestResourceNames();
var stream = assembly.GetManifestResourceStream(name);
return stream ?? throw new FileNotFoundException("Model not found in embedded resources.");
}
}
+23
View File
@@ -0,0 +1,23 @@
global using System.Reflection;
global using BGR.Console.Common;
global using BGR.Console.Removal.Models;
global using BGR.Console.Removal.Onnx;
global using BGR.Console.Resources;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Logging;
global using Microsoft.ML.OnnxRuntime;
global using Microsoft.ML.OnnxRuntime.Tensors;
global using Serilog;
global using Serilog.Events;
global using Serilog.Formatting.Compact;
global using SixLabors.ImageSharp;
global using SixLabors.ImageSharp.Formats.Png;
global using SixLabors.ImageSharp.PixelFormats;
global using SixLabors.ImageSharp.Processing;
global using Spectre.Console.Cli;
+28
View File
@@ -0,0 +1,28 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console", "BGR.Console\BGR.Console.csproj", "{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BGR.Console.Tests", "BGR.Console.Tests\BGR.Console.Tests.csproj", "{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2630F4B7-8188-40DF-BFF0-84B2EA8C9994}.Release|Any CPU.Build.0 = Release|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C5F69AD0-2CE5-4961-A74D-194CF6D812CA}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AnalysisLevel>latest</AnalysisLevel>
<AnalysisMode>All</AnalysisMode>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
</PropertyGroup>
</Project>