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