diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..46d1b58 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,72 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +indent_style = space +tab_width = 4 + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON files +[*.json] +indent_size = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false + +# C# files +[*.cs] + +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true + +# Using directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# Code style +csharp_prefer_braces = true:suggestion +csharp_prefer_simple_using_statement = true:suggestion + +# Naming conventions +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.severity = suggestion +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.symbols = interface +dotnet_naming_rule.interfaces_should_be_prefixed_with_i.style = begins_with_i + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..04641bb --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,43 @@ +name: Build and Test + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + dotnet-version: ['8.0.x'] + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release + + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal + + - name: Pack + if: matrix.os == 'ubuntu-latest' + run: dotnet pack src/BiRefNetSharp/BiRefNetSharp.csproj --configuration Release --no-build --output ./artifacts + + - name: Upload artifacts + if: matrix.os == 'ubuntu-latest' + uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: ./artifacts/*.nupkg diff --git a/BiRefNetSharp.sln b/BiRefNetSharp.sln new file mode 100644 index 0000000..c4e4a59 --- /dev/null +++ b/BiRefNetSharp.sln @@ -0,0 +1,56 @@ +ο»Ώ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiRefNetSharp", "src\BiRefNetSharp\BiRefNetSharp.csproj", "{321C0277-D19B-49C2-BDDB-1A974B8AA3A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5D20AA90-6969-D8BD-9DCD-8634F4692FDA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BiRefNetSharp.Sample", "samples\BiRefNetSharp.Sample\BiRefNetSharp.Sample.csproj", "{53863F60-7577-4E9C-9A9C-0605B6B64E85}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|x64.Build.0 = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Debug|x86.Build.0 = Debug|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|x64.ActiveCfg = Release|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|x64.Build.0 = Release|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|x86.ActiveCfg = Release|Any CPU + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7}.Release|x86.Build.0 = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|x64.ActiveCfg = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|x64.Build.0 = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|x86.ActiveCfg = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Debug|x86.Build.0 = Debug|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|Any CPU.Build.0 = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|x64.ActiveCfg = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|x64.Build.0 = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|x86.ActiveCfg = Release|Any CPU + {53863F60-7577-4E9C-9A9C-0605B6B64E85}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {321C0277-D19B-49C2-BDDB-1A974B8AA3A7} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + {53863F60-7577-4E9C-9A9C-0605B6B64E85} = {5D20AA90-6969-D8BD-9DCD-8634F4692FDA} + EndGlobalSection +EndGlobal diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..27270e9 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to BiRefNetSharp will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2024-12-24 + +### Added +- Initial release of BiRefNetSharp +- Core BiRefNetModel class for ONNX inference +- ImagePreprocessor with ImageNet normalization support +- ImagePostprocessor with mask generation and refinement +- BiRefNetHelper utilities for common operations +- Support for both synchronous and asynchronous operations +- Sample console application demonstrating usage +- Comprehensive documentation and examples +- Support for custom ONNX session options +- Background removal functionality +- Mask extraction and manipulation +- Threshold and smoothing operations +- XML documentation for IntelliSense support + +### Dependencies +- Microsoft.ML.OnnxRuntime 1.19.2 +- SixLabors.ImageSharp 3.1.12 +- .NET 8.0 + +[1.0.0]: https://github.com/dogvane/BiRefNetSharp/releases/tag/v1.0.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..dae0d59 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,111 @@ +# Contributing to BiRefNetSharp + +Thank you for your interest in contributing to BiRefNetSharp! This document provides guidelines for contributing to the project. + +## How to Contribute + +### Reporting Bugs + +If you find a bug, please create an issue on GitHub with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected behavior +- Actual behavior +- Your environment (OS, .NET version, etc.) +- Any relevant code samples or error messages + +### Suggesting Enhancements + +Enhancement suggestions are welcome! Please create an issue with: +- A clear description of the enhancement +- Why it would be useful +- Any implementation ideas you have + +### Pull Requests + +1. Fork the repository +2. Create a new branch for your feature (`git checkout -b feature/amazing-feature`) +3. Make your changes +4. Ensure your code follows the project's coding standards +5. Add or update tests as needed +6. Update documentation if necessary +7. Commit your changes (`git commit -m 'Add amazing feature'`) +8. Push to your branch (`git push origin feature/amazing-feature`) +9. Open a Pull Request + +## Development Setup + +### Prerequisites + +- .NET 8.0 SDK or later +- Git +- Your favorite code editor (Visual Studio, VS Code, Rider, etc.) + +### Building the Project + +```bash +# Clone the repository +git clone https://github.com/dogvane/BiRefNetSharp.git +cd BiRefNetSharp + +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build + +# Run the sample (requires ONNX model) +cd samples/BiRefNetSharp.Sample +dotnet run -- +``` + +## Coding Standards + +### General Guidelines + +- Follow standard C# coding conventions +- Use meaningful variable and method names +- Add XML documentation comments for public APIs +- Keep methods focused and concise +- Handle errors appropriately +- Dispose of resources properly + +### Code Style + +This project uses an `.editorconfig` file to maintain consistent code style. Key points: + +- Use 4 spaces for indentation +- Place opening braces on new lines +- Use `var` when the type is obvious +- Prefer explicit accessibility modifiers +- Use `using` statements for IDisposable objects + +### Documentation + +- Add XML documentation comments for all public types and members +- Include ``, ``, and `` tags as appropriate +- Provide code examples in documentation when helpful +- Update README.md if you add new features +- Add entries to EXAMPLES.md for new usage patterns + +### Testing + +Currently, this project doesn't have automated tests, but we welcome contributions in this area! + +If you add tests: +- Write clear, focused test cases +- Test both success and failure scenarios +- Use descriptive test names +- Mock external dependencies where appropriate + +## License + +By contributing to BiRefNetSharp, you agree that your contributions will be licensed under the MIT License. + +## Questions? + +If you have questions about contributing, feel free to: +- Open an issue for discussion +- Reach out to the maintainers + +Thank you for contributing to BiRefNetSharp! πŸŽ‰ diff --git a/EXAMPLES.md b/EXAMPLES.md new file mode 100644 index 0000000..7173f9f --- /dev/null +++ b/EXAMPLES.md @@ -0,0 +1,411 @@ +# BiRefNetSharp Examples + +This document provides detailed examples for using BiRefNetSharp. + +## Table of Contents + +1. [Basic Usage](#basic-usage) +2. [Advanced Usage](#advanced-usage) +3. [Async Operations](#async-operations) +4. [Custom Processing](#custom-processing) +5. [Batch Processing](#batch-processing) +6. [Performance Tips](#performance-tips) + +## Basic Usage + +### Remove Background from Single Image + +The simplest way to remove background from an image: + +```csharp +using BiRefNetSharp; + +// Load the model once +using var model = new BiRefNetModel("model.onnx"); + +// Remove background +BiRefNetHelper.RemoveBackground( + model: model, + imagePath: "input.jpg", + outputPath: "output.png" +); +``` + +### Extract Segmentation Mask Only + +If you only need the mask without applying it: + +```csharp +using BiRefNetSharp; + +using var model = new BiRefNetModel("model.onnx"); + +BiRefNetHelper.GetMask( + model: model, + imagePath: "input.jpg", + outputPath: "mask.png" +); +``` + +## Advanced Usage + +### Manual Control Over Each Step + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +// Load model +using var model = new BiRefNetModel("model.onnx"); + +// Load image +using var inputImage = Image.Load("input.jpg"); + +// Run inference +using var mask = model.Predict(inputImage); + +// Optional: Apply post-processing +using var smoothedMask = ImagePostprocessor.SmoothMask(mask, sigma: 2.5f); + +// Optional: Create binary mask +using var binaryMask = ImagePostprocessor.ApplyThreshold(smoothedMask, threshold: 128); + +// Apply mask to original image +using var result = BiRefNetModel.ApplyMask(inputImage, smoothedMask); + +// Save result +result.SaveAsPng("output.png"); + +// Or save the mask separately +smoothedMask.SaveAsPng("mask.png"); +``` + +### Custom ONNX Session Options + +```csharp +using BiRefNetSharp; +using Microsoft.ML.OnnxRuntime; + +// Configure ONNX Runtime options +var options = new SessionOptions +{ + // Use GPU if available + // ExecutionMode = ExecutionMode.ORT_PARALLEL, + // GraphOptimizationLevel = GraphOptimizationLevel.ORT_ENABLE_ALL +}; + +using var model = new BiRefNetModel("model.onnx", options); + +// Use the model as normal +``` + +### Custom Input Size + +```csharp +using BiRefNetSharp; +using Microsoft.ML.OnnxRuntime.Tensors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +// Preprocess with custom size +using var image = Image.Load("input.jpg"); +var tensor = ImagePreprocessor.PreprocessImage(image, targetSize: 512); + +// Note: You'll need to run inference manually with this approach +``` + +## Async Operations + +### Async Background Removal + +For responsive applications, use async methods: + +```csharp +using BiRefNetSharp; + +using var model = new BiRefNetModel("model.onnx"); + +await BiRefNetHelper.RemoveBackgroundAsync( + model: model, + imagePath: "input.jpg", + outputPath: "output.png", + cancellationToken: cancellationToken +); +``` + +### Async with Progress Reporting + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using var model = new BiRefNetModel("model.onnx"); + +Console.WriteLine("Loading image..."); +using var image = await Image.LoadAsync("input.jpg"); + +Console.WriteLine("Running inference..."); +using var mask = await model.PredictAsync(image, cancellationToken); + +Console.WriteLine("Applying mask..."); +using var result = BiRefNetModel.ApplyMask(image, mask); + +Console.WriteLine("Saving result..."); +await result.SaveAsPngAsync("output.png"); + +Console.WriteLine("Done!"); +``` + +## Custom Processing + +### Create Binary Mask with Threshold + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using var model = new BiRefNetModel("model.onnx"); +using var image = Image.Load("input.jpg"); + +// Get mask +using var mask = model.Predict(image); + +// Apply different thresholds +using var lowThreshold = ImagePostprocessor.ApplyThreshold(mask, 100); +using var mediumThreshold = ImagePostprocessor.ApplyThreshold(mask, 128); +using var highThreshold = ImagePostprocessor.ApplyThreshold(mask, 180); + +// Save different versions +lowThreshold.SaveAsPng("mask_low.png"); +mediumThreshold.SaveAsPng("mask_medium.png"); +highThreshold.SaveAsPng("mask_high.png"); +``` + +### Apply Different Smoothing Levels + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using var model = new BiRefNetModel("model.onnx"); +using var image = Image.Load("input.jpg"); +using var mask = model.Predict(image); + +// Try different smoothing levels +using var noSmoothing = mask.Clone(); +using var lightSmoothing = ImagePostprocessor.SmoothMask(mask, sigma: 1.0f); +using var mediumSmoothing = ImagePostprocessor.SmoothMask(mask, sigma: 2.0f); +using var heavySmoothing = ImagePostprocessor.SmoothMask(mask, sigma: 4.0f); + +// Apply to image +using var result1 = BiRefNetModel.ApplyMask(image, noSmoothing); +using var result2 = BiRefNetModel.ApplyMask(image, lightSmoothing); +using var result3 = BiRefNetModel.ApplyMask(image, mediumSmoothing); +using var result4 = BiRefNetModel.ApplyMask(image, heavySmoothing); + +result1.SaveAsPng("no_smooth.png"); +result2.SaveAsPng("light_smooth.png"); +result3.SaveAsPng("medium_smooth.png"); +result4.SaveAsPng("heavy_smooth.png"); +``` + +## Batch Processing + +### Process Multiple Images + +```csharp +using BiRefNetSharp; + +// Load model once for all images +using var model = new BiRefNetModel("model.onnx"); + +var imageFiles = Directory.GetFiles("input", "*.jpg"); + +foreach (var imagePath in imageFiles) +{ + var fileName = Path.GetFileNameWithoutExtension(imagePath); + var outputPath = Path.Combine("output", $"{fileName}_nobg.png"); + + Console.WriteLine($"Processing {fileName}..."); + + BiRefNetHelper.RemoveBackground( + model: model, + imagePath: imagePath, + outputPath: outputPath + ); +} + +Console.WriteLine($"Processed {imageFiles.Length} images"); +``` + +### Parallel Batch Processing + +```csharp +using BiRefNetSharp; +using System.Collections.Concurrent; + +var imageFiles = Directory.GetFiles("input", "*.jpg"); +var completedCount = 0; + +// Process in parallel +Parallel.ForEach(imageFiles, new ParallelOptions { MaxDegreeOfParallelism = 4 }, imagePath => +{ + // Each thread gets its own model instance + using var model = new BiRefNetModel("model.onnx"); + + var fileName = Path.GetFileNameWithoutExtension(imagePath); + var outputPath = Path.Combine("output", $"{fileName}_nobg.png"); + + BiRefNetHelper.RemoveBackground( + model: model, + imagePath: imagePath, + outputPath: outputPath + ); + + var count = Interlocked.Increment(ref completedCount); + Console.WriteLine($"Completed {count}/{imageFiles.Length}: {fileName}"); +}); + +Console.WriteLine("All images processed!"); +``` + +### Async Batch Processing with Semaphore + +```csharp +using BiRefNetSharp; + +var imageFiles = Directory.GetFiles("input", "*.jpg"); +var maxConcurrency = 4; +var semaphore = new SemaphoreSlim(maxConcurrency); + +var tasks = imageFiles.Select(async imagePath => +{ + await semaphore.WaitAsync(); + try + { + using var model = new BiRefNetModel("model.onnx"); + + var fileName = Path.GetFileNameWithoutExtension(imagePath); + var outputPath = Path.Combine("output", $"{fileName}_nobg.png"); + + await BiRefNetHelper.RemoveBackgroundAsync( + model: model, + imagePath: imagePath, + outputPath: outputPath + ); + + Console.WriteLine($"Completed: {fileName}"); + } + finally + { + semaphore.Release(); + } +}); + +await Task.WhenAll(tasks); +Console.WriteLine("All images processed!"); +``` + +## Performance Tips + +### 1. Reuse Model Instance + +```csharp +// ❌ Bad: Creating new model for each image +foreach (var image in images) +{ + using var model = new BiRefNetModel("model.onnx"); // Slow! + // ... +} + +// βœ… Good: Reuse model instance +using var model = new BiRefNetModel("model.onnx"); +foreach (var image in images) +{ + // Use same model instance +} +``` + +### 2. Dispose Images Properly + +```csharp +// Use 'using' statements to ensure proper disposal +using var image = Image.Load("input.jpg"); +using var mask = model.Predict(image); +using var result = BiRefNetModel.ApplyMask(image, mask); +result.SaveAsPng("output.png"); +// All resources automatically disposed here +``` + +### 3. Process in Memory Streams + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +using var model = new BiRefNetModel("model.onnx"); + +// Load from stream +using var inputStream = File.OpenRead("input.jpg"); +using var image = Image.Load(inputStream); + +// Process +using var mask = model.Predict(image); +using var result = BiRefNetModel.ApplyMask(image, mask); + +// Save to stream +using var outputStream = File.Create("output.png"); +await result.SaveAsPngAsync(outputStream); +``` + +### 4. Optimize for GPU (if available) + +```csharp +using BiRefNetSharp; +using Microsoft.ML.OnnxRuntime; + +var options = new SessionOptions(); +// Uncomment if you have CUDA support +// options.AppendExecutionProvider_CUDA(0); + +using var model = new BiRefNetModel("model.onnx", options); +``` + +## Error Handling + +### Robust Error Handling + +```csharp +using BiRefNetSharp; + +try +{ + using var model = new BiRefNetModel("model.onnx"); + + BiRefNetHelper.RemoveBackground( + model: model, + imagePath: "input.jpg", + outputPath: "output.png" + ); + + Console.WriteLine("Success!"); +} +catch (FileNotFoundException ex) +{ + Console.WriteLine($"File not found: {ex.Message}"); +} +catch (ArgumentException ex) +{ + Console.WriteLine($"Invalid argument: {ex.Message}"); +} +catch (Exception ex) +{ + Console.WriteLine($"Unexpected error: {ex.Message}"); + Console.WriteLine(ex.StackTrace); +} +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0307574 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 BiRefNetSharp Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index d4ad816..330deb0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,210 @@ # BiRefNetSharp + BiRefNet's ONNX inference implementation under .NET + +## Overview + +BiRefNetSharp is a .NET library that provides ONNX Runtime inference for BiRefNet (Bilateral Reference Network), a state-of-the-art deep learning model for image segmentation and background removal. + +## Features + +- πŸš€ High-performance ONNX Runtime inference +- πŸ–ΌοΈ Image preprocessing with ImageNet normalization +- 🎨 Multiple output options: masks, transparent backgrounds +- πŸ”„ Synchronous and asynchronous APIs +- 🎯 Easy-to-use helper methods +- πŸ“¦ Cross-platform support (.NET 8.0) + +## Requirements + +- .NET 8.0 or higher +- BiRefNet ONNX model file + +## Installation + +### From Source + +```bash +git clone https://github.com/dogvane/BiRefNetSharp.git +cd BiRefNetSharp +dotnet build +``` + +### NuGet Package (Coming Soon) + +```bash +dotnet add package BiRefNetSharp +``` + +## Quick Start + +### Basic Usage + +```csharp +using BiRefNetSharp; + +// Load the model +using var model = new BiRefNetModel("path/to/model.onnx"); + +// Remove background from an image +BiRefNetHelper.RemoveBackground( + model: model, + imagePath: "input.jpg", + outputPath: "output.png" +); +``` + +### Advanced Usage + +```csharp +using BiRefNetSharp; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +// Load the model +using var model = new BiRefNetModel("model.onnx"); + +// Load input image +using var inputImage = Image.Load("input.jpg"); + +// Get segmentation mask +using var mask = model.Predict(inputImage); + +// Apply custom post-processing +using var smoothedMask = ImagePostprocessor.SmoothMask(mask, sigma: 2.0f); +using var binaryMask = ImagePostprocessor.ApplyThreshold(smoothedMask, threshold: 128); + +// Apply mask to original image +using var result = BiRefNetModel.ApplyMask(inputImage, smoothedMask); + +// Save result +result.SaveAsPng("output.png"); +``` + +### Async Operations + +```csharp +using BiRefNetSharp; + +// Load the model +using var model = new BiRefNetModel("model.onnx"); + +// Process image asynchronously +await BiRefNetHelper.RemoveBackgroundAsync( + model: model, + imagePath: "input.jpg", + outputPath: "output.png", + cancellationToken: cancellationToken +); +``` + +## Command Line Sample + +The repository includes a command-line sample application: + +```bash +cd samples/BiRefNetSharp.Sample +dotnet run -- [output_path] +``` + +Example: + +```bash +dotnet run -- model.onnx input.jpg output.png +``` + +## API Documentation + +### BiRefNetModel + +Main class for running BiRefNet inference. + +#### Constructor + +```csharp +public BiRefNetModel(string modelPath, SessionOptions? options = null) +``` + +#### Methods + +- `Image Predict(Image inputImage)` - Run inference and get segmentation mask +- `Task> PredictAsync(Image inputImage, CancellationToken cancellationToken = default)` - Async version +- `static Image ApplyMask(Image inputImage, Image mask)` - Apply mask to image + +### BiRefNetHelper + +Utility methods for common operations. + +#### Methods + +- `RemoveBackground(...)` - Remove background from image (synchronous) +- `RemoveBackgroundAsync(...)` - Remove background from image (asynchronous) +- `GetMask(...)` - Extract segmentation mask only + +### ImagePreprocessor + +Image preprocessing utilities. + +#### Methods + +- `DenseTensor PreprocessImage(Image image, int targetSize = 1024)` - Preprocess image for model input +- `DenseTensor PreprocessImage(string imagePath, int targetSize = 1024)` - Preprocess from file path + +### ImagePostprocessor + +Image postprocessing utilities. + +#### Methods + +- `Image PostprocessOutput(float[] output, int originalWidth, int originalHeight)` - Convert model output to mask +- `Image ApplyThreshold(Image mask, byte threshold = 128)` - Apply binary threshold +- `Image SmoothMask(Image mask, float sigma = 2.0f)` - Smooth mask with Gaussian blur + +## Model Information + +BiRefNetSharp expects an ONNX model exported from BiRefNet. The model should: + +- Accept input tensor of shape `[1, 3, H, W]` (NCHW format) +- Use ImageNet normalization (mean: [0.485, 0.456, 0.406], std: [0.229, 0.224, 0.225]) +- Output tensor of shape `[1, H, W]` or `[H*W]` containing segmentation logits + +Default input size is 1024x1024, but this can be configured. + +## Project Structure + +``` +BiRefNetSharp/ +β”œβ”€β”€ src/ +β”‚ └── BiRefNetSharp/ # Main library +β”‚ β”œβ”€β”€ BiRefNetModel.cs # Core model class +β”‚ β”œβ”€β”€ BiRefNetHelper.cs # Helper utilities +β”‚ β”œβ”€β”€ ImagePreprocessor.cs # Preprocessing +β”‚ └── ImagePostprocessor.cs # Postprocessing +β”œβ”€β”€ samples/ +β”‚ └── BiRefNetSharp.Sample/ # Command-line sample +└── BiRefNetSharp.sln # Solution file +``` + +## Dependencies + +- [Microsoft.ML.OnnxRuntime](https://github.com/microsoft/onnxruntime) - ONNX Runtime for .NET +- [SixLabors.ImageSharp](https://github.com/SixLabors/ImageSharp) - Cross-platform image processing + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Acknowledgments + +- BiRefNet model by [ZhengPeng7/BiRefNet](https://github.com/ZhengPeng7/BiRefNet) +- ONNX Runtime by Microsoft +- ImageSharp by Six Labors + +## Support + +For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/dogvane/BiRefNetSharp). + diff --git a/samples/BiRefNetSharp.Sample/BiRefNetSharp.Sample.csproj b/samples/BiRefNetSharp.Sample/BiRefNetSharp.Sample.csproj new file mode 100644 index 0000000..af5c962 --- /dev/null +++ b/samples/BiRefNetSharp.Sample/BiRefNetSharp.Sample.csproj @@ -0,0 +1,14 @@ +ο»Ώ + + + + + + + Exe + net8.0 + enable + enable + + + diff --git a/samples/BiRefNetSharp.Sample/Program.cs b/samples/BiRefNetSharp.Sample/Program.cs new file mode 100644 index 0000000..222c5f7 --- /dev/null +++ b/samples/BiRefNetSharp.Sample/Program.cs @@ -0,0 +1,73 @@ +ο»Ώusing BiRefNetSharp; + +Console.WriteLine("BiRefNet ONNX Inference Sample"); +Console.WriteLine("==============================\n"); + +// Check command line arguments +if (args.Length < 2) +{ + Console.WriteLine("Usage: BiRefNetSharp.Sample [output_path]"); + Console.WriteLine("\nExample:"); + Console.WriteLine(" BiRefNetSharp.Sample model.onnx input.jpg output.png"); + Console.WriteLine("\nArguments:"); + Console.WriteLine(" model_path - Path to the BiRefNet ONNX model file"); + Console.WriteLine(" image_path - Path to the input image"); + Console.WriteLine(" output_path - (Optional) Path to save the output image with transparent background"); + Console.WriteLine(" If not specified, saves as 'output.png' in current directory"); + return 1; +} + +string modelPath = args[0]; +string imagePath = args[1]; +string outputPath = args.Length > 2 ? args[2] : "output.png"; + +try +{ + // Validate input files + if (!File.Exists(modelPath)) + { + Console.WriteLine($"Error: Model file not found: {modelPath}"); + return 1; + } + + if (!File.Exists(imagePath)) + { + Console.WriteLine($"Error: Image file not found: {imagePath}"); + return 1; + } + + Console.WriteLine($"Model: {modelPath}"); + Console.WriteLine($"Input Image: {imagePath}"); + Console.WriteLine($"Output Image: {outputPath}\n"); + + // Load the model + Console.WriteLine("Loading BiRefNet model..."); + using var model = new BiRefNetModel(modelPath); + Console.WriteLine("Model loaded successfully!\n"); + + // Process the image + Console.WriteLine("Processing image..."); + var startTime = DateTime.Now; + + BiRefNetHelper.RemoveBackground( + model: model, + imagePath: imagePath, + outputPath: outputPath, + smoothMask: true, + maskSigma: 2.0f + ); + + var elapsed = DateTime.Now - startTime; + Console.WriteLine($"Processing completed in {elapsed.TotalSeconds:F2} seconds\n"); + + Console.WriteLine($"Output saved to: {outputPath}"); + Console.WriteLine("\nSuccess!"); + + return 0; +} +catch (Exception ex) +{ + Console.WriteLine($"\nError: {ex.Message}"); + Console.WriteLine($"\nStack trace:\n{ex.StackTrace}"); + return 1; +} diff --git a/src/BiRefNetSharp/BiRefNetHelper.cs b/src/BiRefNetSharp/BiRefNetHelper.cs new file mode 100644 index 0000000..2ebb641 --- /dev/null +++ b/src/BiRefNetSharp/BiRefNetHelper.cs @@ -0,0 +1,162 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BiRefNetSharp; + +/// +/// Provides utility methods for BiRefNet operations. +/// +public static class BiRefNetHelper +{ + /// + /// Removes the background from an image using BiRefNet model. + /// + /// BiRefNet model instance. + /// Path to the input image. + /// Path to save the output image with transparent background. + /// Whether to apply smoothing to the mask. + /// Sigma value for Gaussian blur if smoothing is enabled. + public static void RemoveBackground( + BiRefNetModel model, + string imagePath, + string outputPath, + bool smoothMask = true, + float maskSigma = 2.0f) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (string.IsNullOrWhiteSpace(imagePath)) + { + throw new ArgumentException("Image path cannot be null or empty.", nameof(imagePath)); + } + + if (string.IsNullOrWhiteSpace(outputPath)) + { + throw new ArgumentException("Output path cannot be null or empty.", nameof(outputPath)); + } + + if (!File.Exists(imagePath)) + { + throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); + } + + // Load the input image + using var inputImage = Image.Load(imagePath); + + // Run inference + using var mask = model.Predict(inputImage); + + // Apply smoothing if requested + using var finalMask = smoothMask ? ImagePostprocessor.SmoothMask(mask, maskSigma) : mask.Clone(); + + // Apply mask to get transparent background + using var result = BiRefNetModel.ApplyMask(inputImage, finalMask); + + // Save the result + result.SaveAsPng(outputPath); + } + + /// + /// Removes the background from an image using BiRefNet model asynchronously. + /// + /// BiRefNet model instance. + /// Path to the input image. + /// Path to save the output image with transparent background. + /// Whether to apply smoothing to the mask. + /// Sigma value for Gaussian blur if smoothing is enabled. + /// Cancellation token. + public static async Task RemoveBackgroundAsync( + BiRefNetModel model, + string imagePath, + string outputPath, + bool smoothMask = true, + float maskSigma = 2.0f, + CancellationToken cancellationToken = default) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (string.IsNullOrWhiteSpace(imagePath)) + { + throw new ArgumentException("Image path cannot be null or empty.", nameof(imagePath)); + } + + if (string.IsNullOrWhiteSpace(outputPath)) + { + throw new ArgumentException("Output path cannot be null or empty.", nameof(outputPath)); + } + + if (!File.Exists(imagePath)) + { + throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); + } + + // Load the input image + using var inputImage = await Image.LoadAsync(imagePath, cancellationToken); + + // Run inference + using var mask = await model.PredictAsync(inputImage, cancellationToken); + + // Apply smoothing if requested + using var finalMask = smoothMask ? ImagePostprocessor.SmoothMask(mask, maskSigma) : mask.Clone(); + + // Apply mask to get transparent background + using var result = BiRefNetModel.ApplyMask(inputImage, finalMask); + + // Save the result + await result.SaveAsPngAsync(outputPath, cancellationToken); + } + + /// + /// Gets only the segmentation mask from an image. + /// + /// BiRefNet model instance. + /// Path to the input image. + /// Path to save the mask image. + /// Whether to apply smoothing to the mask. + /// Sigma value for Gaussian blur if smoothing is enabled. + public static void GetMask( + BiRefNetModel model, + string imagePath, + string outputPath, + bool smoothMask = true, + float maskSigma = 2.0f) + { + if (model == null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (string.IsNullOrWhiteSpace(imagePath)) + { + throw new ArgumentException("Image path cannot be null or empty.", nameof(imagePath)); + } + + if (string.IsNullOrWhiteSpace(outputPath)) + { + throw new ArgumentException("Output path cannot be null or empty.", nameof(outputPath)); + } + + if (!File.Exists(imagePath)) + { + throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); + } + + // Load the input image + using var inputImage = Image.Load(imagePath); + + // Run inference + using var mask = model.Predict(inputImage); + + // Apply smoothing if requested + using var finalMask = smoothMask ? ImagePostprocessor.SmoothMask(mask, maskSigma) : mask.Clone(); + + // Save the mask + finalMask.SaveAsPng(outputPath); + } +} diff --git a/src/BiRefNetSharp/BiRefNetModel.cs b/src/BiRefNetSharp/BiRefNetModel.cs new file mode 100644 index 0000000..1fb53c9 --- /dev/null +++ b/src/BiRefNetSharp/BiRefNetModel.cs @@ -0,0 +1,166 @@ +using Microsoft.ML.OnnxRuntime; +using Microsoft.ML.OnnxRuntime.Tensors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + +namespace BiRefNetSharp; + +/// +/// BiRefNet model for image segmentation and background removal using ONNX Runtime. +/// +public class BiRefNetModel : IDisposable +{ + private readonly InferenceSession _session; + private readonly SessionOptions _sessionOptions; + private bool _disposed; + + /// + /// Gets the input name for the ONNX model. + /// + public string InputName { get; } + + /// + /// Gets the output name for the ONNX model. + /// + public string OutputName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Path to the ONNX model file. + /// Optional session options for ONNX Runtime. + public BiRefNetModel(string modelPath, SessionOptions? options = null) + { + if (string.IsNullOrWhiteSpace(modelPath)) + { + throw new ArgumentException("Model path cannot be null or empty.", nameof(modelPath)); + } + + if (!File.Exists(modelPath)) + { + throw new FileNotFoundException($"Model file not found: {modelPath}", modelPath); + } + + _sessionOptions = options ?? new SessionOptions(); + _session = new InferenceSession(modelPath, _sessionOptions); + + // Get input and output names from the model + InputName = _session.InputMetadata.Keys.First(); + OutputName = _session.OutputMetadata.Keys.First(); + } + + /// + /// Runs inference on the input image and returns the segmentation mask. + /// + /// Input image to process. + /// Segmentation mask as a grayscale image. + public Image Predict(Image inputImage) + { + if (inputImage == null) + { + throw new ArgumentNullException(nameof(inputImage)); + } + + // Preprocess the image + var inputTensor = ImagePreprocessor.PreprocessImage(inputImage); + + // Create input for ONNX Runtime + var inputs = new List + { + NamedOnnxValue.CreateFromTensor(InputName, inputTensor) + }; + + // Run inference + using var results = _session.Run(inputs); + var output = results.First().AsEnumerable().ToArray(); + + // Postprocess the output + var mask = ImagePostprocessor.PostprocessOutput(output, inputImage.Width, inputImage.Height); + + return mask; + } + + /// + /// Runs inference on the input image and returns the segmentation mask asynchronously. + /// + /// Input image to process. + /// Cancellation token. + /// Segmentation mask as a grayscale image. + public Task> PredictAsync(Image inputImage, CancellationToken cancellationToken = default) + { + return Task.Run(() => Predict(inputImage), cancellationToken); + } + + /// + /// Applies the segmentation mask to the input image to remove the background. + /// + /// Original input image. + /// Segmentation mask. + /// Image with transparent background. + public static Image ApplyMask(Image inputImage, Image mask) + { + if (inputImage == null) + { + throw new ArgumentNullException(nameof(inputImage)); + } + + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + if (inputImage.Width != mask.Width || inputImage.Height != mask.Height) + { + throw new ArgumentException("Input image and mask must have the same dimensions."); + } + + var result = new Image(inputImage.Width, inputImage.Height); + + inputImage.ProcessPixelRows(mask, result, (inputAccessor, maskAccessor, resultAccessor) => + { + for (int y = 0; y < inputImage.Height; y++) + { + var inputRow = inputAccessor.GetRowSpan(y); + var maskRow = maskAccessor.GetRowSpan(y); + var resultRow = resultAccessor.GetRowSpan(y); + + for (int x = 0; x < inputImage.Width; x++) + { + var pixel = inputRow[x]; + var alpha = maskRow[x].PackedValue; + + resultRow[x] = new Rgba32(pixel.R, pixel.G, pixel.B, alpha); + } + } + }); + + return result; + } + + /// + /// Releases all resources used by the . + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + _session?.Dispose(); + _sessionOptions?.Dispose(); + } + + _disposed = true; + } + } +} diff --git a/src/BiRefNetSharp/BiRefNetSharp.csproj b/src/BiRefNetSharp/BiRefNetSharp.csproj new file mode 100644 index 0000000..08580ba --- /dev/null +++ b/src/BiRefNetSharp/BiRefNetSharp.csproj @@ -0,0 +1,27 @@ +ο»Ώ + + + net8.0 + enable + enable + true + BiRefNetSharp Contributors + BiRefNet's ONNX inference implementation for .NET - Background removal and image segmentation using ONNX Runtime + https://github.com/dogvane/BiRefNetSharp + https://github.com/dogvane/BiRefNetSharp + MIT + birefnet;onnx;image-segmentation;background-removal;machine-learning;computer-vision + README.md + 1.0.0 + + + + + + + + + + + + diff --git a/src/BiRefNetSharp/ImagePostprocessor.cs b/src/BiRefNetSharp/ImagePostprocessor.cs new file mode 100644 index 0000000..0bf6f86 --- /dev/null +++ b/src/BiRefNetSharp/ImagePostprocessor.cs @@ -0,0 +1,170 @@ +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace BiRefNetSharp; + +/// +/// Provides image postprocessing functionality for BiRefNet model output. +/// +public static class ImagePostprocessor +{ + /// + /// Postprocesses the model output to create a segmentation mask. + /// Assumes model output is square (HxH). For non-square outputs, use the overload that accepts explicit dimensions. + /// + /// Raw model output as a float array. + /// Original image width to resize the mask to. + /// Original image height to resize the mask to. + /// Segmentation mask as a grayscale image. + /// Thrown when output array is not square or dimensions are invalid. + public static Image PostprocessOutput(float[] output, int originalWidth, int originalHeight) + { + if (output == null || output.Length == 0) + { + throw new ArgumentException("Output array cannot be null or empty.", nameof(output)); + } + + if (originalWidth <= 0 || originalHeight <= 0) + { + throw new ArgumentException("Image dimensions must be positive."); + } + + // Calculate the dimensions of the output (assume square) + int outputSize = (int)Math.Sqrt(output.Length); + + // Validate that the output is actually square + if (outputSize * outputSize != output.Length) + { + throw new ArgumentException( + $"Output array length ({output.Length}) is not a perfect square. " + + "Use the overload that accepts explicit width and height for non-square outputs.", + nameof(output)); + } + + return PostprocessOutput(output, outputSize, outputSize, originalWidth, originalHeight); + } + + /// + /// Postprocesses the model output to create a segmentation mask with explicit output dimensions. + /// + /// Raw model output as a float array. + /// Width of the model output tensor. + /// Height of the model output tensor. + /// Target width to resize the mask to. + /// Target height to resize the mask to. + /// Segmentation mask as a grayscale image. + public static Image PostprocessOutput( + float[] output, + int outputWidth, + int outputHeight, + int targetWidth, + int targetHeight) + { + if (output == null || output.Length == 0) + { + throw new ArgumentException("Output array cannot be null or empty.", nameof(output)); + } + + if (outputWidth <= 0 || outputHeight <= 0) + { + throw new ArgumentException("Output dimensions must be positive."); + } + + if (targetWidth <= 0 || targetHeight <= 0) + { + throw new ArgumentException("Target dimensions must be positive."); + } + + if (output.Length != outputWidth * outputHeight) + { + throw new ArgumentException( + $"Output array length ({output.Length}) does not match expected dimensions " + + $"({outputWidth}x{outputHeight}={outputWidth * outputHeight})."); + } + + // Create a temporary mask image at model output size + var tempMask = new Image(outputWidth, outputHeight); + + // Apply sigmoid activation and convert to byte values + tempMask.ProcessPixelRows(accessor => + { + for (int y = 0; y < outputHeight; y++) + { + var pixelRow = accessor.GetRowSpan(y); + + for (int x = 0; x < outputWidth; x++) + { + int index = y * outputWidth + x; + float value = output[index]; + + // Apply sigmoid activation + float sigmoid = 1.0f / (1.0f + MathF.Exp(-value)); + + // Convert to byte [0, 255] + byte pixelValue = (byte)(sigmoid * 255f); + + pixelRow[x] = new L8(pixelValue); + } + } + }); + + // Resize to target dimensions if needed + if (outputWidth != targetWidth || outputHeight != targetHeight) + { + tempMask.Mutate(ctx => ctx.Resize(targetWidth, targetHeight)); + } + + return tempMask; + } + + /// + /// Applies a threshold to the mask to create a binary segmentation. + /// + /// Input mask image. + /// Threshold value (0-255). Pixels below this value become black, above become white. + /// Thresholded binary mask. + public static Image ApplyThreshold(Image mask, byte threshold = 128) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + var result = mask.Clone(); + + result.ProcessPixelRows(accessor => + { + for (int y = 0; y < result.Height; y++) + { + var pixelRow = accessor.GetRowSpan(y); + + for (int x = 0; x < result.Width; x++) + { + var pixel = pixelRow[x]; + pixelRow[x] = new L8(pixel.PackedValue >= threshold ? (byte)255 : (byte)0); + } + } + }); + + return result; + } + + /// + /// Applies smoothing to the mask to reduce noise. + /// + /// Input mask image. + /// Gaussian blur sigma value. + /// Smoothed mask. + public static Image SmoothMask(Image mask, float sigma = 2.0f) + { + if (mask == null) + { + throw new ArgumentNullException(nameof(mask)); + } + + var result = mask.Clone(); + result.Mutate(ctx => ctx.GaussianBlur(sigma)); + return result; + } +} diff --git a/src/BiRefNetSharp/ImagePreprocessor.cs b/src/BiRefNetSharp/ImagePreprocessor.cs new file mode 100644 index 0000000..45b3310 --- /dev/null +++ b/src/BiRefNetSharp/ImagePreprocessor.cs @@ -0,0 +1,97 @@ +using Microsoft.ML.OnnxRuntime.Tensors; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace BiRefNetSharp; + +/// +/// Provides image preprocessing functionality for BiRefNet model input. +/// +public static class ImagePreprocessor +{ + /// + /// Default input size for the BiRefNet model. + /// + public const int DefaultInputSize = 1024; + + /// + /// Mean values for image normalization (ImageNet statistics). + /// + private static readonly float[] Mean = { 0.485f, 0.456f, 0.406f }; + + /// + /// Standard deviation values for image normalization (ImageNet statistics). + /// + private static readonly float[] Std = { 0.229f, 0.224f, 0.225f }; + + /// + /// Preprocesses an image for BiRefNet model input. + /// + /// Input image to preprocess. + /// Target size for the model input (default: 1024x1024). + /// Preprocessed tensor ready for model inference. + public static DenseTensor PreprocessImage(Image image, int targetSize = DefaultInputSize) + { + if (image == null) + { + throw new ArgumentNullException(nameof(image)); + } + + if (targetSize <= 0) + { + throw new ArgumentException("Target size must be positive.", nameof(targetSize)); + } + + // Clone and resize the image + var resizedImage = image.Clone(ctx => ctx.Resize(targetSize, targetSize)); + + // Create tensor with shape [1, 3, height, width] (NCHW format) + var tensor = new DenseTensor(new[] { 1, 3, targetSize, targetSize }); + + // Process each pixel and normalize + resizedImage.ProcessPixelRows(accessor => + { + for (int y = 0; y < targetSize; y++) + { + var pixelRow = accessor.GetRowSpan(y); + + for (int x = 0; x < targetSize; x++) + { + var pixel = pixelRow[x]; + + // Normalize to [0, 1] and apply ImageNet normalization + tensor[0, 0, y, x] = (pixel.R / 255f - Mean[0]) / Std[0]; + tensor[0, 1, y, x] = (pixel.G / 255f - Mean[1]) / Std[1]; + tensor[0, 2, y, x] = (pixel.B / 255f - Mean[2]) / Std[2]; + } + } + }); + + resizedImage.Dispose(); + + return tensor; + } + + /// + /// Preprocesses an image from a file path. + /// + /// Path to the image file. + /// Target size for the model input (default: 1024x1024). + /// Preprocessed tensor ready for model inference. + public static DenseTensor PreprocessImage(string imagePath, int targetSize = DefaultInputSize) + { + if (string.IsNullOrWhiteSpace(imagePath)) + { + throw new ArgumentException("Image path cannot be null or empty.", nameof(imagePath)); + } + + if (!File.Exists(imagePath)) + { + throw new FileNotFoundException($"Image file not found: {imagePath}", imagePath); + } + + using var image = Image.Load(imagePath); + return PreprocessImage(image, targetSize); + } +}