diff --git a/.gitignore b/.gitignore
index bc78471..29273b5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -482,3 +482,10 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
+
+test_input_*
+test_output_*
+ImageConvolution/Agent_Processed_Output/
+ImageConvolution/Processed_Output_Par/
+ImageConvolution/Processed_Output_Seq/
+ImageConvolution/Processed_Output_*/
diff --git a/ImageConvolution.Tests/ConvolutionTests.cs b/ImageConvolution.Tests/ConvolutionTests.cs
new file mode 100644
index 0000000..96bdc9f
--- /dev/null
+++ b/ImageConvolution.Tests/ConvolutionTests.cs
@@ -0,0 +1,239 @@
+using System;
+using System.IO;
+
+using ImageConvolution;
+
+using Xunit;
+
+namespace ImageConvolution.Tests;
+
+public class ConvolutionTests
+{
+ [Fact]
+ public void Test_ImageIO_SaveAndLoad()
+ {
+ string testPath = "test_image.png";
+ double[,] originalData = new double[2, 2] { { 0, 255 }, { 128, 64 } };
+
+ try
+ {
+ ImageIO.SaveImage(originalData, testPath);
+ Assert.True(File.Exists(testPath));
+
+ double[,] loadedData = ImageIO.LoadAsGrayscale(testPath);
+
+ Assert.Equal(2, loadedData.GetLength(0));
+ Assert.Equal(2, loadedData.GetLength(1));
+ }
+ finally
+ {
+ if (File.Exists(testPath)) File.Delete(testPath);
+ }
+ }
+
+ [Fact]
+ public void Test_Convolve_ExtendStrategy()
+ {
+ double[,] image = { { 10, 10, 10 }, { 10, 10, 10 }, { 10, 10, 10 } };
+ double[,] result = ConvolutionProcessor.Convolve(image, Kernels.BlurBox, EdgeStrategy.Extend);
+ Assert.Equal(10, Math.Round(result[1, 1]));
+ }
+
+ [Fact]
+ public void Test_Convolve_ZeroPaddingStrategy()
+ {
+ double[,] image = { { 10, 10, 10 }, { 10, 10, 10 }, { 10, 10, 10 } };
+ double[,] result = ConvolutionProcessor.Convolve(image, Kernels.BlurBox, EdgeStrategy.ZeroPadding);
+ Assert.True(result[0, 0] < 10);
+ }
+
+ [Fact]
+ public void Test_ConvolveParallel()
+ {
+ double[,] image = { { 100, 100, 100 }, { 100, 100, 100 }, { 100, 100, 100 } };
+ double[,] result = ParallelConvolutionProcessor.ConvolveParallel(image, Kernels.BlurBox, EdgeStrategy.Extend);
+ Assert.Equal(100, Math.Round(result[1, 1]));
+ }
+
+ [Fact]
+ public void Test_KernelsExist()
+ {
+ Assert.NotNull(Kernels.BlurBox);
+ Assert.NotNull(Kernels.Sharpen);
+ Assert.NotNull(Kernels.Laplacian);
+ }
+
+ [Fact]
+ public void Test_ConvolveParallel_ZeroPadding()
+ {
+ double[,] image = {
+ { 10, 10, 10 },
+ { 10, 10, 10 },
+ { 10, 10, 10 }
+ };
+
+ double[,] result = ParallelConvolutionProcessor.ConvolveParallel(
+ image,
+ Kernels.BlurBox,
+ EdgeStrategy.ZeroPadding);
+
+ Assert.True(result[0, 0] < 10);
+ }
+
+ private void CreateTestImage(string directory, string filename)
+ {
+ Directory.CreateDirectory(directory);
+ double[,] dummyPixelData = new double[2, 2] { { 128, 128 }, { 128, 128 } };
+ string fullPath = Path.Combine(directory, filename);
+ ImageIO.SaveImage(dummyPixelData, fullPath);
+ }
+
+ [Fact]
+ public void Test_BatchProcessor_Naive_Sequential()
+ {
+ string inputDir = "test_input_naive_seq";
+ string outputDir = "test_output_naive_seq";
+
+ try
+ {
+ CreateTestImage(inputDir, "test1.jpg");
+
+ BatchProcessor.ProcessImagesNaiveParallel(inputDir, outputDir, false);
+
+ Assert.True(Directory.Exists(outputDir));
+ Assert.Single(Directory.GetFiles(outputDir));
+ }
+ finally
+ {
+ if (Directory.Exists(inputDir)) Directory.Delete(inputDir, true);
+ if (Directory.Exists(outputDir)) Directory.Delete(outputDir, true);
+ }
+ }
+
+ [Fact]
+ public void Test_BatchProcessor_Naive_Parallel()
+ {
+ string inputDir = "test_input_naive_par";
+ string outputDir = "test_output_naive_par";
+
+ try
+ {
+ CreateTestImage(inputDir, "test1.jpg");
+
+ BatchProcessor.ProcessImagesNaiveParallel(inputDir, outputDir, true);
+
+ Assert.True(Directory.Exists(outputDir));
+ Assert.Single(Directory.GetFiles(outputDir));
+ }
+ finally
+ {
+ if (Directory.Exists(inputDir)) Directory.Delete(inputDir, true);
+ if (Directory.Exists(outputDir)) Directory.Delete(outputDir, true);
+ }
+ }
+
+ [Fact]
+ public void Test_AgentProcessor_Success()
+ {
+ string inputDir = "test_input_agents";
+ string outputDir = "test_output_agents";
+
+ try
+ {
+ CreateTestImage(inputDir, "test1.jpg");
+ CreateTestImage(inputDir, "test2.jpg");
+
+ AgentProcessor.ProcessImagesWithAgents(inputDir, outputDir, workerCount: 2);
+
+ Assert.True(Directory.Exists(outputDir));
+ Assert.Equal(2, Directory.GetFiles(outputDir).Length);
+ }
+ finally
+ {
+ if (Directory.Exists(inputDir)) Directory.Delete(inputDir, true);
+ if (Directory.Exists(outputDir)) Directory.Delete(outputDir, true);
+ }
+ }
+
+ [Fact]
+ public void Test_BatchProcessor_NonExistentFolder()
+ {
+ string nonExistentDir = "non_existent_folder_abc_123";
+ string outputDir = "output_abc";
+
+ BatchProcessor.ProcessImagesNaiveParallel(nonExistentDir, outputDir, false);
+
+ Assert.False(Directory.Exists(outputDir));
+ }
+
+ [Fact]
+ public void Test_AgentProcessor_NonExistentFolder()
+ {
+ string nonExistentDir = "non_existent_folder_xyz_999";
+ string outputDir = "output_xyz";
+
+ AgentProcessor.ProcessImagesWithAgents(nonExistentDir, outputDir, 2);
+
+ Assert.False(Directory.Exists(outputDir));
+ }
+
+ [Fact]
+ public void Test_CustomKernelSize_5x5()
+ {
+ double[,] image = new double[10, 10];
+ for (int i = 0; i < 10; i++)
+ for (int j = 0; j < 10; j++) image[i, j] = 100;
+
+ double[,] kernel5x5 = new double[5, 5];
+ for (int i = 0; i < 5; i++)
+ for (int j = 0; j < 5; j++) kernel5x5[i, j] = 1.0 / 25.0;
+
+ double[,] result = ConvolutionProcessor.Convolve(image, kernel5x5, EdgeStrategy.Extend);
+ Assert.Equal(100, Math.Round(result[5, 5]));
+ }
+
+ [Fact]
+ public void Test_SharpenAndLaplacianKernels()
+ {
+ double[,] image = { { 50, 50, 50 }, { 50, 50, 50 }, { 50, 50, 50 } };
+
+ var resLapl = ConvolutionProcessor.Convolve(image, Kernels.Laplacian);
+ var resSharp = ConvolutionProcessor.Convolve(image, Kernels.Sharpen);
+
+ Assert.Equal(0, Math.Round(resLapl[1, 1]));
+ Assert.Equal(50, Math.Round(resSharp[1, 1]));
+ }
+
+ private void CreateLargeTestImage(string directory, string filename)
+ {
+ Directory.CreateDirectory(directory);
+ double[,] dummyPixelData = new double[20, 20];
+ for (int y = 0; y < 20; y++)
+ for (int x = 0; x < 20; x++)
+ dummyPixelData[y, x] = 128;
+
+ string fullPath = Path.Combine(directory, filename);
+ ImageIO.SaveImage(dummyPixelData, fullPath);
+ }
+
+ [Fact]
+ public void Test_LibraryProcessor_Parallel_Success()
+ {
+ string inputDir = "test_input_lib_par";
+ string outputDir = "test_output_lib_par";
+ try
+ {
+ CreateLargeTestImage(inputDir, "lib_test.jpg");
+
+ LibraryProcessor.ProcessImagesWithImageSharp(inputDir, outputDir);
+
+ Assert.True(Directory.Exists(outputDir));
+ Assert.Single(Directory.GetFiles(outputDir));
+ }
+ finally
+ {
+ if (Directory.Exists(inputDir)) Directory.Delete(inputDir, true);
+ if (Directory.Exists(outputDir)) Directory.Delete(outputDir, true);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution.Tests/GpuConvolutionTests.cs b/ImageConvolution.Tests/GpuConvolutionTests.cs
new file mode 100644
index 0000000..12a1ad3
--- /dev/null
+++ b/ImageConvolution.Tests/GpuConvolutionTests.cs
@@ -0,0 +1,124 @@
+using System;
+using System.IO;
+
+using Xunit;
+
+namespace ImageConvolution.Tests
+{
+ public class GpuConvolutionTests : IDisposable
+ {
+ private const string TestDirInput = "gpu_test_input";
+ private const string TestDirOutput = "gpu_test_output";
+
+ public GpuConvolutionTests()
+ {
+ Cleanup();
+ }
+
+ private void Cleanup()
+ {
+ if (Directory.Exists(TestDirInput)) Directory.Delete(TestDirInput, true);
+ if (Directory.Exists(TestDirOutput)) Directory.Delete(TestDirOutput, true);
+ }
+
+ public void Dispose()
+ {
+ Cleanup();
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public void GpuProcessor_CanBeConstructedAndDisposed()
+ {
+ using var gpuProcessor = new GpuConvolutionProcessor();
+ Assert.NotNull(gpuProcessor);
+ }
+
+ [Fact]
+ public void ConvolveGpu_ExtendStrategy_ProducesCorrectCenterPixel()
+ {
+ using var gpuProcessor = new GpuConvolutionProcessor();
+ var image = new float[5, 5];
+ for (int i = 0; i < 5; i++)
+ for (int j = 0; j < 5; j++)
+ image[i, j] = 100f;
+
+ var result = gpuProcessor.ConvolveGpu(image, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Assert.Equal(100, Math.Round(result[2, 2]));
+ }
+
+ [Fact]
+ public void ConvolveGpu_ZeroPaddingStrategy_AffectsCornerPixel()
+ {
+ using var gpuProcessor = new GpuConvolutionProcessor();
+ var image = new float[5, 5];
+ for (int i = 0; i < 5; i++)
+ for (int j = 0; j < 5; j++)
+ image[i, j] = 100f;
+
+ var result = gpuProcessor.ConvolveGpu(image, Kernels.BlurBoxFloat, EdgeStrategy.ZeroPadding);
+
+ Assert.True(result[0, 0] < 50);
+ }
+
+ [Fact]
+ public void ProcessDirectory_ProcessesFilesCorrectly()
+ {
+ CreateTestImageFloat(TestDirInput, "test1.jpg", 10, 10);
+ CreateTestImageFloat(TestDirInput, "test2.jpg", 10, 10);
+
+ GpuConvolutionProcessor.ProcessDirectory(TestDirInput, TestDirOutput, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Assert.True(Directory.Exists(TestDirOutput));
+ Assert.Equal(2, Directory.GetFiles(TestDirOutput).Length);
+ Assert.True(File.Exists(Path.Combine(TestDirOutput, "test1.jpg")));
+ Assert.True(File.Exists(Path.Combine(TestDirOutput, "test2.jpg")));
+ }
+
+ [Fact]
+ public void ProcessDirectory_SkipsMismatchedSizeFiles()
+ {
+ CreateTestImageFloat(TestDirInput, "test_10x10.jpg", 10, 10);
+ CreateTestImageFloat(TestDirInput, "test_20x20.jpg", 20, 20);
+
+ GpuConvolutionProcessor.ProcessDirectory(TestDirInput, TestDirOutput, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Assert.True(Directory.Exists(TestDirOutput));
+ Assert.Single(Directory.GetFiles(TestDirOutput));
+ Assert.True(File.Exists(Path.Combine(TestDirOutput, "test_10x10.jpg")));
+ Assert.False(File.Exists(Path.Combine(TestDirOutput, "test_20x20.jpg")));
+ }
+
+ [Fact]
+ public void ProcessDirectory_HandlesNonExistentInputDirectory()
+ {
+ GpuConvolutionProcessor.ProcessDirectory("non_existent_dir_12345", TestDirOutput, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Assert.False(Directory.Exists(TestDirOutput));
+ }
+
+ [Fact]
+ public void ProcessDirectory_HandlesEmptyInputDirectory()
+ {
+ Directory.CreateDirectory(TestDirInput);
+
+ GpuConvolutionProcessor.ProcessDirectory(TestDirInput, TestDirOutput, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Assert.True(Directory.Exists(TestDirOutput));
+ Assert.Empty(Directory.GetFiles(TestDirOutput));
+ }
+
+ private void CreateTestImageFloat(string directory, string filename, int width, int height)
+ {
+ Directory.CreateDirectory(directory);
+ var dummyPixelData = new float[height, width];
+ for (int y = 0; y < height; y++)
+ for (int x = 0; x < width; x++)
+ dummyPixelData[y, x] = 128f;
+
+ string fullPath = Path.Combine(directory, filename);
+ ImageIO.SaveImageFloat(dummyPixelData, fullPath);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution.Tests/ImageConvolution.Tests.csproj b/ImageConvolution.Tests/ImageConvolution.Tests.csproj
new file mode 100644
index 0000000..5e99567
--- /dev/null
+++ b/ImageConvolution.Tests/ImageConvolution.Tests.csproj
@@ -0,0 +1,32 @@
+
+
+
+ net9.0
+ enable
+ enable
+ false
+ true
+
+ **/GpuConvolutionProcessor.cs
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ImageConvolution/AgentProcessor.cs b/ImageConvolution/AgentProcessor.cs
new file mode 100644
index 0000000..a15a755
--- /dev/null
+++ b/ImageConvolution/AgentProcessor.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Threading.Tasks;
+
+using Microsoft.VisualBasic;
+
+namespace ImageConvolution
+{
+ public class ImageToProcess
+ {
+ public string FilePath { get; set; } = "";
+ }
+
+ public class ImageResult
+ {
+ public string OriginalFileName { get; set; } = "";
+ public double[,] ProcessedData { get; set; } = new double[0, 0];
+ }
+
+
+ public class AgentProcessor
+ {
+ public static void ProcessImagesWithAgents(string inputDir, string outputDir, int workerCount)
+ {
+ Console.WriteLine($"Запуск конвейера с {workerCount} агентами");
+ if (!Directory.Exists(inputDir)) return;
+ Directory.CreateDirectory(outputDir);
+
+ var filesToProcessQueue = new BlockingCollection(100);
+ var processedImagesQueue = new BlockingCollection(100);
+
+ var stopwatch = Stopwatch.StartNew();
+ var readerAgent = Task.Run(() =>
+ {
+
+ foreach (var file in Directory.GetFiles(inputDir, "*jpg"))
+ {
+ filesToProcessQueue.Add(new ImageToProcess { FilePath = file });
+ }
+
+ filesToProcessQueue.CompleteAdding();
+ });
+
+ var workerAgents = new List();
+ for (int i = 0; i < workerCount; i++)
+ {
+ var worker = Task.Run(() =>
+ {
+ foreach (var imageToProcess in filesToProcessQueue.GetConsumingEnumerable())
+ {
+ double[,] image = ImageIO.LoadAsGrayscale(imageToProcess.FilePath);
+ double[,] processedImage = ConvolutionProcessor.Convolve(image, Kernels.BlurBox);
+ var ResultForQue = new ImageResult
+ {
+ OriginalFileName = Path.GetFileName(imageToProcess.FilePath),
+ ProcessedData = processedImage
+ };
+ processedImagesQueue.Add(ResultForQue);
+ }
+ });
+ workerAgents.Add(worker);
+ }
+
+ var allWorkersTask = Task.WhenAll(workerAgents).ContinueWith(t =>
+ {
+ processedImagesQueue.CompleteAdding();
+ });
+
+
+ var writerAgent = Task.Run(() =>
+ {
+
+ foreach (var result in processedImagesQueue.GetConsumingEnumerable())
+ {
+ string savePath = Path.Combine(outputDir, result.OriginalFileName);
+ ImageIO.SaveImage(result.ProcessedData, savePath);
+ }
+ });
+
+ Task.WaitAll(readerAgent, allWorkersTask, writerAgent);
+
+ stopwatch.Stop();
+ Console.WriteLine($"\nОбработка конвейером завершена за {stopwatch.ElapsedMilliseconds} мс.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution/BatchProcessor.cs b/ImageConvolution/BatchProcessor.cs
new file mode 100644
index 0000000..4643e86
--- /dev/null
+++ b/ImageConvolution/BatchProcessor.cs
@@ -0,0 +1,53 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Runtime.Intrinsics.Arm;
+using System.Threading.Tasks;
+
+namespace ImageConvolution
+{
+ public class BatchProcessor
+ {
+
+ public static void ProcessImagesNaiveParallel(string inputDirectory, string outputDirectory, bool useParallelConvolutionInside)
+ {
+ if (!Directory.Exists(inputDirectory))
+ {
+ Console.WriteLine("Ошибка: Указанная папка не существует.");
+ return;
+ }
+
+ if (!Directory.Exists(outputDirectory))
+ {
+ Directory.CreateDirectory(outputDirectory);
+ }
+
+ string[] files = Directory.GetFiles(inputDirectory, "*.jpg");
+
+ Console.WriteLine($"Найдено файлов: {files.Length}");
+
+
+ Parallel.ForEach(files, currentFile =>
+ {
+ string fileName = Path.GetFileName(currentFile);
+
+ string savePath = Path.Combine(outputDirectory, fileName);
+
+ double[,] imageData = ImageIO.LoadAsGrayscale(currentFile);
+
+ double[,] result;
+ if (useParallelConvolutionInside)
+ {
+ result = ParallelConvolutionProcessor.ConvolveParallel(imageData, Kernels.BlurBox);
+ }
+ else
+ {
+ result = ConvolutionProcessor.Convolve(imageData, Kernels.BlurBox);
+ }
+
+ ImageIO.SaveImage(result, savePath);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution/GpuConvolutionProcessor.cs b/ImageConvolution/GpuConvolutionProcessor.cs
new file mode 100644
index 0000000..3e55ca4
--- /dev/null
+++ b/ImageConvolution/GpuConvolutionProcessor.cs
@@ -0,0 +1,201 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+
+using ILGPU;
+using ILGPU.Runtime;
+using ILGPU.Runtime.Cuda;
+
+namespace ImageConvolution
+{
+ public class GpuConvolutionProcessor : IDisposable
+ {
+ private readonly Context context;
+ public readonly Accelerator accelerator;
+ private readonly Action, ArrayView2D, ArrayView2D, int, int, int> convolutionKernel;
+
+ public GpuConvolutionProcessor()
+ {
+ context = Context.Create(builder => builder.Cuda().Optimize(OptimizationLevel.O2));
+
+ var gpuDevice = context.GetPreferredDevice(false);
+ if (gpuDevice != null && gpuDevice.AcceleratorType == AcceleratorType.Cuda)
+ {
+ accelerator = gpuDevice.CreateAccelerator(context);
+ }
+ else
+ {
+ Console.WriteLine("Предупреждение: CUDA-совместимый GPU не найден. Попытка использовать эмулятор CPU.");
+ var cpuDevice = context.Devices.FirstOrDefault(d => d.AcceleratorType == AcceleratorType.CPU);
+ if (cpuDevice != null)
+ {
+ accelerator = cpuDevice.CreateAccelerator(context);
+ }
+ else
+ {
+ throw new InvalidOperationException("Не найден ни CUDA GPU, ни CPU Accelerator.");
+ }
+ }
+
+ Console.WriteLine($"Используется устройство: {accelerator.Name}");
+ convolutionKernel = accelerator.LoadAutoGroupedStreamKernel, ArrayView2D, ArrayView2D, int, int, int>(ConvolutionKernel);
+ }
+
+ static void ConvolutionKernel(
+ Index2D index,
+ ArrayView2D input,
+ ArrayView2D output,
+ ArrayView2D kernel,
+ int strategy,
+ int offsetX,
+ int offsetY)
+ {
+ int x = index.X;
+ int y = index.Y;
+
+ int imgHeight = input.IntExtent.Y;
+ int imgWidth = input.IntExtent.X;
+ int kHeight = kernel.IntExtent.Y;
+ int kWidth = kernel.IntExtent.X;
+
+ float sum = 0.0f;
+
+ for (int ky = 0; ky < kHeight; ky++)
+ {
+ for (int kx = 0; kx < kWidth; kx++)
+ {
+ int pixelY = y + ky - offsetY;
+ int pixelX = x + kx - offsetX;
+
+ float val = 0.0f;
+
+ if (strategy == 0)
+ {
+ int clampedY = pixelY < 0 ? 0 : (pixelY >= imgHeight ? imgHeight - 1 : pixelY);
+ int clampedX = pixelX < 0 ? 0 : (pixelX >= imgWidth ? imgWidth - 1 : pixelX);
+ val = input[clampedY, clampedX];
+ }
+ else
+ {
+ if (pixelY >= 0 && pixelY < imgHeight && pixelX >= 0 && pixelX < imgWidth)
+ {
+ val = input[pixelY, pixelX];
+ }
+ }
+ sum += val * kernel[ky, kx];
+ }
+ }
+ output[y, x] = sum;
+ }
+
+ public float[,] ConvolveGpu(float[,] image, float[,] kernelData, EdgeStrategy strategy)
+ {
+ int height = image.GetLength(0);
+ int width = image.GetLength(1);
+
+ using var sourceBuffer = accelerator.Allocate2DDenseX(new LongIndex2D(height, width));
+ using var targetBuffer = accelerator.Allocate2DDenseX(new LongIndex2D(height, width));
+ using var kernelBuffer = accelerator.Allocate2DDenseX(new LongIndex2D(kernelData.GetLength(0), kernelData.GetLength(1)));
+
+ sourceBuffer.CopyFromCPU(image);
+ kernelBuffer.CopyFromCPU(kernelData);
+
+ int offsetX = kernelData.GetLength(1) / 2;
+ int offsetY = kernelData.GetLength(0) / 2;
+
+ convolutionKernel(new Index2D(width, height), sourceBuffer.View, targetBuffer.View, kernelBuffer.View, (int)strategy, offsetX, offsetY);
+
+ return targetBuffer.GetAsArray2D();
+ }
+
+ public static void ProcessDirectory(string inputDirectory, string outputDirectory, float[,] kernel, EdgeStrategy strategy)
+ {
+ if (!Directory.Exists(inputDirectory))
+ {
+ Console.WriteLine("Ошибка: Указанная папка не существует.");
+ return;
+ }
+ Directory.CreateDirectory(outputDirectory);
+
+ string[] files = Directory.GetFiles(inputDirectory, "*.jpg").OrderBy(f => f).ToArray();
+ if (files.Length == 0)
+ {
+ Console.WriteLine("Файлы .jpg не найдены.");
+ return;
+ }
+
+ Console.WriteLine($"Найдено файлов для обработки на GPU: {files.Length}");
+
+ using var gpuProcessor = new GpuConvolutionProcessor();
+
+ float[,] firstImage = ImageIO.LoadAsGrayscaleFloat(files[0]);
+ int height = firstImage.GetLength(0);
+ int width = firstImage.GetLength(1);
+
+ using var sourceBuffer = gpuProcessor.accelerator.Allocate2DDenseX(new LongIndex2D(height, width));
+ using var targetBuffer = gpuProcessor.accelerator.Allocate2DDenseX(new LongIndex2D(height, width));
+ using var kernelBuffer = gpuProcessor.accelerator.Allocate2DDenseX(new LongIndex2D(kernel.GetLength(0), kernel.GetLength(1)));
+
+ kernelBuffer.CopyFromCPU(kernel);
+ int offsetX = kernel.GetLength(1) / 2;
+ int offsetY = kernel.GetLength(0) / 2;
+
+ var totalStopwatch = Stopwatch.StartNew();
+ long fullGpuPipelineTimeMs = 0;
+ long computeOnlyTimeMs = 0;
+
+ for (int i = 0; i < files.Length; i++)
+ {
+ float[,] imageData;
+ if (i == 0)
+ {
+ imageData = firstImage;
+ }
+ else
+ {
+ imageData = ImageIO.LoadAsGrayscaleFloat(files[i]);
+ }
+
+ if (imageData.GetLength(0) != height || imageData.GetLength(1) != width)
+ {
+ Console.WriteLine($"\nПропуск файла {Path.GetFileName(files[i])} из-за другого размера.");
+ continue;
+ }
+
+ var fullPipelineStopwatch = Stopwatch.StartNew();
+ sourceBuffer.CopyFromCPU(imageData);
+ gpuProcessor.convolutionKernel(new Index2D(width, height), sourceBuffer.View, targetBuffer.View, kernelBuffer.View, (int)strategy, offsetX, offsetY);
+ var resultData = targetBuffer.GetAsArray2D();
+ fullPipelineStopwatch.Stop();
+ fullGpuPipelineTimeMs += fullPipelineStopwatch.ElapsedMilliseconds;
+
+ var computeOnlyStopwatch = Stopwatch.StartNew();
+ sourceBuffer.CopyFromCPU(imageData);
+ gpuProcessor.convolutionKernel(new Index2D(width, height), sourceBuffer.View, targetBuffer.View, kernelBuffer.View, (int)strategy, offsetX, offsetY);
+ gpuProcessor.accelerator.Synchronize();
+ computeOnlyStopwatch.Stop();
+ computeOnlyTimeMs += computeOnlyStopwatch.ElapsedMilliseconds;
+
+ string fileName = Path.GetFileName(files[i]);
+ string savePath = Path.Combine(outputDirectory, fileName);
+ ImageIO.SaveImageFloat(resultData, savePath);
+
+ Console.Write($"\rОбработано {i + 1}/{files.Length}...");
+ }
+
+ totalStopwatch.Stop();
+ Console.WriteLine("\n\nРезультаты:");
+ Console.WriteLine($" Общее время (включая I/O): {totalStopwatch.ElapsedMilliseconds} мс");
+ Console.WriteLine($" Полный GPU-пайплайн (копирование туда-обратно + ядро): {fullGpuPipelineTimeMs} мс (~{fullGpuPipelineTimeMs / (double)files.Length:F2} мс/фото)");
+ Console.WriteLine($" GPU-вычисления (копирование НА GPU + ядро, без обратного копирования): {computeOnlyTimeMs} мс (~{computeOnlyTimeMs / (double)files.Length:F2} мс/фото)");
+ }
+
+ public void Dispose()
+ {
+ accelerator?.Dispose();
+ context?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution/ImageConvolution.csproj b/ImageConvolution/ImageConvolution.csproj
new file mode 100644
index 0000000..2f978a5
--- /dev/null
+++ b/ImageConvolution/ImageConvolution.csproj
@@ -0,0 +1,15 @@
+
+
+
+ Exe
+ net9.0
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/ImageConvolution/LibraryProcessor.cs b/ImageConvolution/LibraryProcessor.cs
new file mode 100644
index 0000000..da6d23f
--- /dev/null
+++ b/ImageConvolution/LibraryProcessor.cs
@@ -0,0 +1,38 @@
+using System;
+using System.IO;
+
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+using SixLabors.ImageSharp.Processing;
+
+namespace ImageConvolution
+{
+ public class LibraryProcessor
+ {
+ public static void ProcessImagesWithImageSharp(string inputDirectory, string outputDirectory)
+ {
+ if (!Directory.Exists(inputDirectory))
+ {
+ Console.WriteLine("Ошибка: папка не найдена.");
+ return;
+ }
+
+ if (!Directory.Exists(outputDirectory))
+ {
+ Directory.CreateDirectory(outputDirectory);
+ }
+
+ string[] files = Directory.GetFiles(inputDirectory, "*.jpg");
+
+ Parallel.ForEach(files, file =>
+ {
+ using Image image = Image.Load(file);
+
+ image.Mutate(x => x.GaussianBlur(1f));
+
+ string savePath = Path.Combine(outputDirectory, Path.GetFileName(file));
+ image.Save(savePath);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/ImageConvolution/Program.cs b/ImageConvolution/Program.cs
new file mode 100644
index 0000000..f4834ae
--- /dev/null
+++ b/ImageConvolution/Program.cs
@@ -0,0 +1,501 @@
+using System;
+using System.Data;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Threading.Tasks;
+
+using SixLabors.ImageSharp;
+using SixLabors.ImageSharp.PixelFormats;
+
+namespace ImageConvolution
+{
+ public enum EdgeStrategy
+ {
+ Extend,
+ ZeroPadding
+ }
+
+ [ExcludeFromCodeCoverage]
+ class Program
+ {
+ static void Main(string[] args)
+ {
+
+ Console.WriteLine("=== Программа свёртки изображений ===");
+ Console.WriteLine("1. Обработать один файл");
+ Console.WriteLine("2. Обработать набор файлов");
+ Console.WriteLine("3. Обработать набор файлов агентами");
+ Console.WriteLine("4. Сравнить с библиотекой ImageSharp");
+ Console.WriteLine("5. Обработать файл на GPU");
+ Console.WriteLine("6. Создать 4K тестовые изображения");
+ string? choice = Console.ReadLine();
+
+ if (choice == "1")
+ {
+ Console.Write("Введите путь к изображению: ");
+ string? inputPath = Console.ReadLine()?.Trim('\"', ' ', '\'');
+ if (!File.Exists(inputPath)) return;
+
+ double[,] img = ImageIO.LoadAsGrayscale(inputPath);
+
+ double seqMs = MeasureMedianMs(() =>
+ {
+ _ = ConvolutionProcessor.Convolve(img, Kernels.BlurBox, EdgeStrategy.Extend);
+ });
+
+ double parMs = MeasureMedianMs(() =>
+ {
+ _ = ParallelConvolutionProcessor.ConvolveParallel(img, Kernels.BlurBox, EdgeStrategy.Extend);
+ });
+
+ Console.WriteLine($"Compute only (median): seq={seqMs:F3} ms, parallel={parMs:F3} ms");
+ double[,] res = ConvolutionProcessor.Convolve(img, Kernels.BlurBox);
+
+ string directory = Path.GetDirectoryName(inputPath)!;
+ string fileNameOnly = Path.GetFileNameWithoutExtension(inputPath);
+ string extension = Path.GetExtension(inputPath);
+
+ string baseOutputPath = Path.Combine(directory, $"{fileNameOnly}_filtered");
+
+ int counter = 1;
+ string finalOutputPath = $"{baseOutputPath}_{counter}{extension}";
+
+ while (File.Exists(finalOutputPath))
+ {
+ counter++;
+ finalOutputPath = $"{baseOutputPath}_{counter}{extension}";
+ }
+
+ ImageIO.SaveImage(res, finalOutputPath);
+ Console.WriteLine($"\nГотово! Изображение сохранено здесь:\n{finalOutputPath}");
+ }
+ else if (choice == "2")
+ {
+ Console.WriteLine("Введите исходный путь к папке или перетащите её");
+ string? inputDir = Console.ReadLine()?.Trim('\"', ' ', '\'');
+
+ if (string.IsNullOrEmpty(inputDir)) return;
+
+ string outputDir = Path.Combine(Path.GetDirectoryName(inputDir) ?? "", "Processed_Output");
+
+ Console.WriteLine("Прогрев...");
+ BatchProcessor.ProcessImagesNaiveParallel(inputDir, outputDir + "_Warmup", false);
+
+ Console.WriteLine("\n--- ТЕСТ 1: Только внешний параллелизм (последовательная свёртка) ---");
+ var times1 = MeasureMultipleRuns(() =>
+ {
+ BatchProcessor.ProcessImagesNaiveParallel(inputDir, outputDir + "_Seq", false);
+ }, 3);
+
+ Console.WriteLine($" Среднее: {times1.Average():F1} мс");
+ Console.WriteLine($" Медиана: {CalculateMedian(times1):F1} мс");
+ Console.WriteLine($" Станд. отклонение: {CalculateStdDev(times1):F1} мс");
+
+ Console.WriteLine("\n--- ТЕСТ 2: Вложенный параллелизм (параллельная свёртка) ---");
+ var times2 = MeasureMultipleRuns(() =>
+ {
+ BatchProcessor.ProcessImagesNaiveParallel(inputDir, outputDir + "_Par", true);
+ }, 3);
+
+ Console.WriteLine($" Среднее: {times2.Average():F1} мс");
+ Console.WriteLine($" Медиана: {CalculateMedian(times2):F1} мс");
+ Console.WriteLine($" Станд. отклонение: {CalculateStdDev(times2):F1} мс");
+ }
+ else if (choice == "3")
+ {
+ Console.WriteLine("Введите путь к папке:");
+ string? inputDir = Console.ReadLine()?.Trim('\"', ' ', '\'');
+ if (string.IsNullOrEmpty(inputDir) || !Directory.Exists(inputDir)) return;
+
+ Console.Write("Введите количество агентов для свёртки (от количества ядер на устройстве): ");
+ if (!int.TryParse(Console.ReadLine(), out int workerCount))
+ {
+ workerCount = Environment.ProcessorCount;
+ }
+
+ string outputDir = Path.Combine(Path.GetDirectoryName(inputDir)!, "Agent_Processed_Output");
+
+ Console.WriteLine("Прогрев...");
+ AgentProcessor.ProcessImagesWithAgents(inputDir, outputDir + "_Warmup", workerCount);
+
+ Console.WriteLine($"\n--- Обработка агентами ({workerCount} агентов) ---");
+ var times = MeasureMultipleRuns(() =>
+ {
+ AgentProcessor.ProcessImagesWithAgents(inputDir, outputDir, workerCount);
+ }, 3);
+
+ Console.WriteLine($" Среднее: {times.Average():F1} мс");
+ Console.WriteLine($" Медиана: {CalculateMedian(times):F1} мс");
+ Console.WriteLine($" Станд. отклонение: {CalculateStdDev(times):F1} мс");
+ }
+ else if (choice == "4")
+ {
+ Console.WriteLine("Введите путь к папке:");
+ string? inputDir = Console.ReadLine()?.Trim('\"', ' ', '\'');
+ if (string.IsNullOrEmpty(inputDir) || !Directory.Exists(inputDir)) return;
+
+ string outputDir = Path.Combine(Path.GetDirectoryName(inputDir)!, "ImageSharp_Output");
+
+ double ms = MeasureMedianMs(() =>
+ {
+ if (Directory.Exists(outputDir))
+ {
+ Directory.Delete(outputDir, true);
+ }
+
+ LibraryProcessor.ProcessImagesWithImageSharp(inputDir, outputDir);
+ });
+
+ Console.WriteLine($"ImageSharp blur (median): {ms:F3} ms");
+ }
+
+ else if (choice == "5")
+ {
+ Console.Write("Введите путь к файлу или папке для обработки на GPU: ");
+ string? path = Console.ReadLine()?.Trim('\"', ' ', '\'');
+
+ if (string.IsNullOrEmpty(path)) return;
+
+ if (File.Exists(path))
+ {
+ Console.WriteLine("\nРежим: обработка одного файла на GPU.");
+ float[,] img = ImageIO.LoadAsGrayscaleFloat(path);
+
+ using var gpuProcessor = new GpuConvolutionProcessor();
+
+ Console.WriteLine("Прогрев GPU...");
+ _ = gpuProcessor.ConvolveGpu(img, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+
+ Console.WriteLine("\n--- Замер производительности свёртки на GPU ---");
+ double gpuMs = MeasureMedianMs(() =>
+ {
+ _ = gpuProcessor.ConvolveGpu(img, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+ }, warmupRuns: 1, measuredRuns: 5);
+
+ Console.WriteLine($"\nРезультат: время свёртки (медиана): {gpuMs:F3} мс");
+
+ string dir = Path.GetDirectoryName(path)!;
+ string fname = Path.GetFileNameWithoutExtension(path);
+ string ext = Path.GetExtension(path);
+ string outputPath = Path.Combine(dir, $"{fname}_gpu_processed{ext}");
+
+ float[,] res = gpuProcessor.ConvolveGpu(img, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+ ImageIO.SaveImageFloat(res, outputPath);
+ Console.WriteLine($"Результат сохранен в: {outputPath}");
+ }
+ else if (Directory.Exists(path))
+ {
+ Console.WriteLine("\nРежим: пакетная обработка папки на GPU.");
+ string outputDir = Path.Combine(Path.GetDirectoryName(path)!, "GPU_Batch_Output");
+
+ GpuConvolutionProcessor.ProcessDirectory(path, outputDir, Kernels.BlurBoxFloat, EdgeStrategy.Extend);
+ }
+ }
+
+ else if (choice == "6")
+ {
+ string outputDir = @"C:\Test4K";
+ Directory.CreateDirectory(outputDir);
+
+ int count = 100;
+ Console.WriteLine($"Создание {count} изображений 4K...");
+
+ for (int i = 0; i < count; i++)
+ {
+ double[,] image = new double[2160, 3840];
+ var rand = new Random(i);
+ for (int y = 0; y < 2160; y++)
+ {
+ for (int x = 0; x < 3840; x++)
+ {
+ image[y, x] = rand.Next(0, 256);
+ }
+ }
+ ImageIO.SaveImage(image, Path.Combine(outputDir, $"test_{i:000}.jpg"));
+ Console.WriteLine($"Создано: {i + 1}/{count}");
+ }
+ Console.WriteLine($"Готово, Папка: {outputDir}");
+ }
+
+ else
+ {
+ Console.WriteLine("Неверный выбор.");
+ }
+
+ }
+ static double MeasureMedianMs(Action action, int warmupRuns = 2, int measuredRuns = 5)
+ {
+ var currentProcess = Process.GetCurrentProcess();
+ var oldPriority = currentProcess.PriorityClass;
+
+ try
+ {
+ currentProcess.PriorityClass = ProcessPriorityClass.High;
+
+ for (int i = 0; i < warmupRuns; i++)
+ {
+ action();
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+ }
+
+ var times = new List();
+
+ for (int i = 0; i < measuredRuns; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ long startTimestamp = Stopwatch.GetTimestamp();
+ action();
+ long endTimestamp = Stopwatch.GetTimestamp();
+
+ double elapsedMs = (endTimestamp - startTimestamp) * 1000.0 / Stopwatch.Frequency;
+ times.Add(elapsedMs);
+ }
+
+ return CalculateMedian(times);
+ }
+ finally
+ {
+ currentProcess.PriorityClass = oldPriority;
+ }
+ }
+ static List MeasureMultipleRuns(Action action, int runs)
+ {
+ var times = new List();
+
+ for (int i = 0; i < runs; i++)
+ {
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ GC.Collect();
+
+ long startTimestamp = Stopwatch.GetTimestamp();
+ action();
+ long endTimestamp = Stopwatch.GetTimestamp();
+
+ double elapsedMs = (endTimestamp - startTimestamp) * 1000.0 / Stopwatch.Frequency;
+ times.Add(elapsedMs);
+ Console.WriteLine($" Прогон {i + 1}: {elapsedMs:F1} мс");
+ }
+
+ return times;
+ }
+
+ static double CalculateMedian(List values)
+ {
+ var sorted = values.OrderBy(x => x).ToList();
+ int count = sorted.Count;
+
+ if (count % 2 == 0)
+ {
+ return (sorted[count / 2 - 1] + sorted[count / 2]) / 2.0;
+ }
+ else
+ {
+ return sorted[count / 2];
+ }
+ }
+
+ static double CalculateStdDev(List values)
+ {
+ double avg = values.Average();
+ double sumOfSquares = values.Sum(val => (val - avg) * (val - avg));
+ return Math.Sqrt(sumOfSquares / values.Count);
+ }
+
+ }
+
+ public class ImageIO
+ {
+ public static double[,] LoadAsGrayscale(string path)
+ {
+ using Image image = Image.Load(path);
+ int width = image.Width;
+ int height = image.Height;
+ double[,] result = new double[height, width];
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ Rgba32 pixel = image[x, y];
+ double gray = 0.299 * pixel.R + 0.587 * pixel.G + 0.114 * pixel.B;
+ result[y, x] = gray;
+ }
+ }
+ return result;
+ }
+ public static void SaveImage(double[,] data, string path)
+ {
+ int height = data.GetLength(0);
+ int width = data.GetLength(1);
+
+ using Image image = new Image(width, height);
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ double val = data[y, x];
+ byte gray = (byte)Math.Clamp(val, 0, 255);
+ image[x, y] = new Rgba32(gray, gray, gray);
+ }
+ }
+ image.Save(path);
+ }
+ public static float[,] LoadAsGrayscaleFloat(string path)
+ {
+ using Image image = Image.Load(path);
+ int width = image.Width;
+ int height = image.Height;
+ float[,] result = new float[height, width];
+
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ Rgba32 pixel = image[x, y];
+ float gray = 0.299f * pixel.R + 0.587f * pixel.G + 0.114f * pixel.B;
+ result[y, x] = gray;
+ }
+ }
+ return result;
+ }
+
+ public static void SaveImageFloat(float[,] data, string path)
+ {
+ int height = data.GetLength(0);
+ int width = data.GetLength(1);
+
+ using Image image = new Image(width, height);
+ for (int y = 0; y < height; y++)
+ {
+ for (int x = 0; x < width; x++)
+ {
+ float val = data[y, x];
+ byte gray = (byte)Math.Clamp(val, 0, 255);
+ image[x, y] = new Rgba32(gray, gray, gray);
+ }
+ }
+ image.Save(path);
+ }
+ }
+
+ public class Kernels
+ {
+ public static double[,] Sharpen = new double[,]
+ {
+ { 0, -1, 0},
+ { -1, 5, -1},
+ { 0, -1, 0}
+ };
+ public static double[,] Laplacian = new double[,]
+ {
+ {-1, -1, -1},
+ {-1, 8, -1},
+ {-1, -1, -1}
+ };
+ public static double[,] BlurBox = new double[,]
+ {
+ {1.0/9.0, 1.0/9.0, 1.0/9.0},
+ {1.0/9.0, 1.0/9.0, 1.0/9.0},
+ {1.0/9.0, 1.0/9.0, 1.0/9.0}
+ };
+ public static float[,] SharpenFloat = new float[,]
+ {
+ { 0, -1, 0},
+ { -1, 5, -1},
+ { 0, -1, 0}
+ };
+ public static float[,] LaplacianFloat = new float[,]
+ {
+ {-1, -1, -1},
+ {-1, 8, -1},
+ {-1, -1, -1}
+ };
+ public static float[,] BlurBoxFloat = new float[,]
+ {
+ {1.0f/9.0f, 1.0f/9.0f, 1.0f/9.0f},
+ {1.0f/9.0f, 1.0f/9.0f, 1.0f/9.0f},
+ {1.0f/9.0f, 1.0f/9.0f, 1.0f/9.0f}
+ };
+ }
+
+ public class ConvolutionProcessor
+ {
+ internal static double CalculatePixelValue(double[,] image, double[,] kernel, int x, int y, EdgeStrategy strategy)
+ {
+ double sum = 0.0;
+ int imgheight = image.GetLength(0);
+ int imgwidth = image.GetLength(1);
+ int kheight = kernel.GetLength(0);
+ int kwidth = kernel.GetLength(1);
+ int offsetY = kheight / 2;
+ int offsetX = kwidth / 2;
+
+ for (int ky = 0; ky < kheight; ky++)
+ {
+ for (int kx = 0; kx < kwidth; kx++)
+ {
+ int pixelY = y + ky - offsetY;
+ int pixelX = x + kx - offsetX;
+ double pixelValue = 0.0;
+
+ if (strategy == EdgeStrategy.Extend)
+ {
+ pixelY = Math.Clamp(pixelY, 0, imgheight - 1);
+ pixelX = Math.Clamp(pixelX, 0, imgwidth - 1);
+ pixelValue = image[pixelY, pixelX];
+ }
+ else if (strategy == EdgeStrategy.ZeroPadding)
+ {
+ if (pixelY >= 0 && pixelY < imgheight && pixelX >= 0 && pixelX < imgwidth)
+ {
+ pixelValue = image[pixelY, pixelX];
+ }
+ }
+ sum += pixelValue * kernel[ky, kx];
+ }
+ }
+ return sum;
+ }
+
+ public static double[,] Convolve(double[,] image, double[,] kernel, EdgeStrategy strategy = EdgeStrategy.Extend)
+ {
+ int imgheight = image.GetLength(0);
+ int imgwidth = image.GetLength(1);
+ double[,] result = new double[imgheight, imgwidth];
+ for (int y = 0; y < imgheight; y++)
+ {
+ for (int x = 0; x < imgwidth; x++)
+ {
+ result[y, x] = CalculatePixelValue(image, kernel, x, y, strategy);
+ }
+ }
+ return result;
+ }
+ }
+ public class ParallelConvolutionProcessor
+ {
+
+ public static double[,] ConvolveParallel(double[,] image, double[,] kernal, EdgeStrategy strategy = EdgeStrategy.Extend)
+ {
+ int imgheight = image.GetLength(0);
+ int imgwidth = image.GetLength(1);
+ double[,] result = new double[imgheight, imgwidth];
+
+ Parallel.For(0, imgheight, y =>
+ {
+ for (int x = 0; x < imgwidth; x++)
+ {
+ result[y, x] = ConvolutionProcessor.CalculatePixelValue(image, kernal, x, y, strategy);
+ }
+ });
+ return result;
+ }
+ }
+}
\ No newline at end of file