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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# This workflow will build a .NET project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net

name: .NET

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: Install OpenCV
run: sudo apt-get update && sudo apt-get install -y libjpeg-dev libpng-dev libtiff-dev libavcodec-dev libavformat-dev libswscale-dev libv4l-dev libxvidcore-dev libx264-dev libopencv-dev
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Test
run: dotnet test --no-build --verbosity normal
8 changes: 8 additions & 0 deletions Convolver.sln
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E7170ACE-9AA
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Core", "src\BMPConvolver.Core\BMPConvolver.Core.csproj", "{523C0934-7353-45C5-AB89-DFB54C98071F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Cli", "src\BMPConvolver.Cli\BMPConvolver.Cli.csproj", "{A99E9047-15E4-492A-8C77-B2C82CAF0B0B}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BMPConvolver.Tests", "test\BMPConvolver.Tests\BMPConvolver.Tests.csproj", "{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -35,5 +41,7 @@ Global
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{523C0934-7353-45C5-AB89-DFB54C98071F} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80}
{A99E9047-15E4-492A-8C77-B2C82CAF0B0B} = {E7170ACE-9AAD-4689-ADC6-22E048D93B80}
{69A51A1D-CE2E-4C75-AD2E-64ADC5F8E593} = {5F4B7EAD-0AB8-4FBF-85F1-EA0BEE09832F}
EndGlobalSection
EndGlobal
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,23 @@

Для запуска приложения выполните следующую команду:

```bash
dotnet run --project src/BMPConvolver.Cli/BMPConvolver.Cli.csproj <input.bmp> <output.bmp> [опции]
```

Опции включают:
- `--mode seq|par`: режим выполнения (последовательный или параллельный)
- `--partition pixels|rows|cols|grid`: способ разделения работы
- `--grid <block width>x<block height>`: размер сетки для grid partition
- `--border zero|clamp`: режим обработки границ
- `--kernel box3|sharpen|identity`: предустановленный kernel
- `--kernel-text "..."`: kernel в текстовом формате
- `--kernel-file path.txt`: kernel из файла

## Тесты

Для запуска тестов:

```bash
dotnet test
```
18 changes: 18 additions & 0 deletions src/BMPConvolver.Cli/BMPConvolver.Cli.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\BMPConvolver.Core\BMPConvolver.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
66 changes: 66 additions & 0 deletions src/BMPConvolver.Cli/KernelParser/KernelTextParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using System.Globalization;
using BMPConvolver.Core;

namespace BMPConvolver.Cli.KernelParser;

public static class KernelTextParser
{
/// <summary>
/// Parses a 2D kernel from text.
/// Rows can be separated by ';' or newlines, values separated by whitespace and/or ','.
/// Center defaults to (width/2, height/2) unless specified.
/// </summary>
public static Kernel Parse(string text, int? centerX = null, int? centerY = null)
{
ArgumentNullException.ThrowIfNull(text);

var rows = new List<float[]>();

foreach (var line in SplitLinesAndSemicolons(text))
{
var values = line
.Replace(',', ' ')
.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(t => float.Parse(t, CultureInfo.InvariantCulture))
.ToArray();

if (values.Length == 0) continue;
rows.Add(values);
}

if (rows.Count == 0) throw new FormatException("Kernel text has no numeric rows.");

var width = rows[0].Length;
if (width == 0) throw new FormatException("Kernel width must be > 0.");

var height = rows.Count;
for (var i = 1; i < height; i++)
if (rows[i].Length != width)
throw new FormatException("All kernel rows must have the same number of values.");


var cx = centerX ?? (width / 2);
var cy = centerY ?? (height / 2);

var weights = new float[width * height];
for (var y = 0; y < height; y++)
{
var row = rows[y];
Array.Copy(row, 0, weights, y * width, width);
}

return new Kernel(width, height, cx, cy, weights);
}

private static IEnumerable<string> SplitLinesAndSemicolons(string text)
{
// Normalize CRLF/CR -> LF first, then split further by ';'
var normalized = text.Replace("\r\n", "\n").Replace('\r', '\n');
foreach (var line in normalized.Split('\n'))
{
var parts = line.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries );
foreach (var p in parts)
yield return p;
}
}
}
97 changes: 97 additions & 0 deletions src/BMPConvolver.Cli/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using BMPConvolver.Core;
using BMPConvolver.Core.ImageSharp;
using BMPConvolver.Cli.KernelParser;

static int PrintUsage()
{
Console.Error.WriteLine("Usage:");
Console.Error.WriteLine(" BMPConvolver.Cli <input.bmp> <output.bmp> [--border zero|clamp]");
Console.Error.WriteLine(" [--kernel box3|sharpen|identity] [--kernel-text \"...\"] [--kernel-file path.txt]");
Console.Error.WriteLine();
Console.Error.WriteLine("Kernel text format:");
Console.Error.WriteLine(" Rows separated by ';' or newlines, values by spaces/commas.");
return 2;
}

if (args.Length < 2) return PrintUsage();

var inputPath = args[0];
var outputPath = args[1];

var border = "zero";
string? kernelPreset = null;
string? kernelText = null;
string? kernelFile = null;

for (var i = 2; i < args.Length; i++)
{
var a = args[i];
if (a == "--border" && i + 1 < args.Length) { border = args[++i]; continue; }
if (a == "--kernel" && i + 1 < args.Length) { kernelPreset = args[++i]; continue; }
if (a == "--kernel-text" && i + 1 < args.Length) { kernelText = args[++i]; continue; }
if (a == "--kernel-file" && i + 1 < args.Length) { kernelFile = args[++i]; continue; }

return PrintUsage();
}

BorderMode borderMode;
Kernel kernel;
try
{
borderMode = border.ToLowerInvariant() switch
{
"zero" => BorderMode.Zero,
"clamp" => BorderMode.Clamp,
_ => throw new ArgumentException($"Unknown border mode: {border}")
};

kernel = ResolveKernel(kernelPreset, kernelText, kernelFile);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
return 2;
}

static Kernel ResolveKernel(string? preset, string? text, string? file)
{
var specified = 0;
if (!string.IsNullOrWhiteSpace(preset)) specified++;
if (!string.IsNullOrWhiteSpace(text)) specified++;
if (!string.IsNullOrWhiteSpace(file)) specified++;
if (specified > 1) throw new ArgumentException("You can specify only one of --kernel, --kernel-text, --kernel-file.");

if (!string.IsNullOrWhiteSpace(text))
return KernelTextParser.Parse(text);

if (!string.IsNullOrWhiteSpace(file))
return KernelTextParser.Parse(File.ReadAllText(file));

var p = (preset ?? "box3").Trim().ToLowerInvariant();
return p switch
{
"identity" => Kernel.Identity(),
"box3" => Kernel.BoxBlur(),
"sharpen" => Kernel.Sharpen(),
_ => throw new ArgumentException($"Unknown kernel preset: {preset}. Use box3|sharpen|identity.")
};
}

var input = GrayImageIo.LoadAsGray(inputPath);

var timer = System.Diagnostics.Stopwatch.StartNew();
GrayImage output;
try
{
output = Convolver.ConvolveSequential(input, kernel, borderMode);
}
catch (Exception e)
{
Console.Error.WriteLine(e.Message);
return 2;
}
timer.Stop();

GrayImageIo.SaveGrayAsBmp(output, outputPath);
Console.WriteLine($"Done. {input.Width}x{input.Height}, border={border}, elapsed={timer.ElapsedMilliseconds} ms");
return 0;
8 changes: 8 additions & 0 deletions src/BMPConvolver.Core/BorderMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace BMPConvolver.Core;

public enum BorderMode
{
Zero = 0,
Clamp = 1,
}

55 changes: 55 additions & 0 deletions src/BMPConvolver.Core/Convolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace BMPConvolver.Core;

public static class Convolver
{
public static GrayImage ConvolveSequential(GrayImage input, Kernel kernel, BorderMode borderMode)
{
ArgumentNullException.ThrowIfNull(input);
ArgumentNullException.ThrowIfNull(kernel);

var output = new float[input.Pixels.Length];
ConvolveInternal(input, output, kernel, borderMode);
return new GrayImage(input.Width, input.Height, output);
}

private static void ConvolveInternal(GrayImage input, float[] output, Kernel kernel, BorderMode borderMode)
{
var src = input.Pixels;
var weights = kernel.Weights;

var yEnd = input.Height;
var xEnd = input.Width;

for (var y = 0; y < yEnd; y++)
for (var x = 0; x < xEnd; x++)
{
var sum = 0f;

for (var ky = 0; ky < kernel.Height; ky++)
{
var iy = y + (ky - kernel.CenterY);
var yInRange = (uint)iy < (uint)input.Height;
if (!yInRange && borderMode == BorderMode.Zero) continue;
if (!yInRange && borderMode == BorderMode.Clamp) iy = iy < 0 ? 0 : (input.Height - 1);

var srcRow = iy * input.Width;
var kRow = ky * kernel.Width;

for (var kx = 0; kx < kernel.Width; kx++)
{
var ix = x + (kx - kernel.CenterX);
var xInRange = (uint)ix < (uint)input.Width;
if (!xInRange && borderMode == BorderMode.Zero) continue;
if (!xInRange && borderMode == BorderMode.Clamp) ix = ix < 0 ? 0 : (input.Width - 1);

var pixel = src[srcRow + ix];
var weight = weights[kRow + kx];
sum += pixel * weight;
}
}

output[(y * input.Width) + x] = sum;
}
}
}

49 changes: 49 additions & 0 deletions src/BMPConvolver.Core/ImageIO/GrayImageIO.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;

namespace BMPConvolver.Core.ImageSharp;

public static class GrayImageIo
{
public static GrayImage LoadAsGray(string path)
{
using var image = Image.Load<L8>(path);
var pixels = new float[image.Width * image.Height];
var bytes = new byte[pixels.Length];

image.CopyPixelDataTo(bytes);
for (var i = 0; i < bytes.Length; i++)
pixels[i] = bytes[i] / 255f;

return new GrayImage(image.Width, image.Height, pixels);
}

public static void SaveGrayAsBmp(GrayImage image, string path)
{
using var outImage = new Image<L8>(image.Width, image.Height);
var src = image.Pixels;
var dest = new L8[src.Length];

// Convert float to L8
for (int i = 0; i < src.Length; i++)
{
float v = Math.Clamp(src[i], 0f, 1f);
dest[i] = new L8((byte)MathF.Round(v * 255f));
}

outImage.ProcessPixelRows(accessor =>
{
int offset = 0;
int width = image.Width;

for (var y = 0; y < image.Height; y++)
{
dest.AsSpan(offset, width).CopyTo(accessor.GetRowSpan(y));
offset += width;
}
});

outImage.SaveAsBmp(path);
}
}

Loading
Loading