diff --git a/tasks/batkov_f_linear_image_filtering/common/include/common.hpp b/tasks/batkov_f_linear_image_filtering/common/include/common.hpp new file mode 100644 index 0000000000..84af2855e7 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/common/include/common.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "task/include/task.hpp" + +namespace batkov_f_linear_image_filtering { + +struct Image { + std::vector data; + size_t width{}; + size_t height{}; + size_t channels{}; +}; + +using InType = Image; +using OutType = Image; +using TestType = std::tuple; +using BaseTask = ppc::task::Task; +using Kernel = std::vector>; +using ImageData = std::vector; + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/info.json b/tasks/batkov_f_linear_image_filtering/info.json new file mode 100644 index 0000000000..c4d3000124 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/info.json @@ -0,0 +1,9 @@ +{ + "student": { + "first_name": "Филипп", + "last_name": "Батьков", + "middle_name": "Владиславович", + "group_number": "3823Б1ПР3", + "task_number": "3" + } +} diff --git a/tasks/batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp b/tasks/batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp new file mode 100644 index 0000000000..d1ddb3c978 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace batkov_f_linear_image_filtering { + +class BatkovFLinearImageFilteringMPI : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kMPI; + } + explicit BatkovFLinearImageFilteringMPI(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + Kernel kernel_; +}; + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/mpi/src/ops_mpi.cpp b/tasks/batkov_f_linear_image_filtering/mpi/src/ops_mpi.cpp new file mode 100644 index 0000000000..e5e6b7a6b1 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/mpi/src/ops_mpi.cpp @@ -0,0 +1,194 @@ +#include "batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp" + +#include + +#include +#include +#include +#include +#include + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" + +namespace batkov_f_linear_image_filtering { + +namespace { + +float ApplyKernel(const Kernel &kernel, const Image &image, size_t row, size_t col, size_t ch) { + const size_t width = image.width; + const size_t height = image.height; + const size_t channels = image.channels; + + float sum = 0.0F; + + for (size_t ky = 0; ky < 3; ky++) { + for (size_t kx = 0; kx < 3; kx++) { + size_t py = row + ky - 1; + size_t px = col + kx - 1; + + py = std::max(py, 0); + py = std::min(py, height - 1); + px = std::max(px, 0); + px = std::min(px, width - 1); + + auto index = (((py * width) + px) * channels) + ch; + sum += static_cast(image.data[index]) * kernel[ky][kx]; + } + } + + return sum; +} + +void CopyBlockData(const Image &image, Image &block, size_t start_row) { + for (size_t row = 0; row < block.height; row++) { + size_t global_row = start_row + row; + + for (size_t col = 0; col < image.width; col++) { + for (size_t ch = 0; ch < image.channels; ch++) { + size_t local_index = ((row * image.width + col) * image.channels) + ch; + size_t global_index = ((global_row * image.width + col) * image.channels) + ch; + + block.data[local_index] = image.data[global_index]; + } + } + } +} + +void ProcessBlock(const Image &block, Image &result_block, const Kernel &kernel, size_t start_row, size_t local_start) { + for (size_t row = 0; row < result_block.height; row++) { + size_t row_in_block = row + (start_row - local_start); + + for (size_t col = 0; col < block.width; col++) { + for (size_t ch = 0; ch < block.channels; ch++) { + float val = ApplyKernel(kernel, block, row_in_block, col, ch); + size_t index = ((row * block.width + col) * block.channels) + ch; + result_block.data[index] = static_cast(std::clamp(val, 0.0F, 255.0F)); + } + } + } +} + +void CopyBlockToOutput(const Image &result_block, Image &output, size_t start_row) { + for (size_t row = 0; row < result_block.height; row++) { + for (size_t col = 0; col < result_block.width; col++) { + for (size_t ch = 0; ch < result_block.channels; ch++) { + size_t output_index = (((start_row + row) * result_block.width + col) * result_block.channels) + ch; + size_t block_index = ((row * result_block.width + col) * result_block.channels) + ch; + output.data[output_index] = result_block.data[block_index]; + } + } + } +} + +void GatherResultsFromProcesses(size_t mpi_size, size_t rows_per_process, size_t remainder, const Image &result_block, + size_t start_row, Image &output) { + CopyBlockToOutput(result_block, output, start_row); + + for (size_t proc = 1; proc < mpi_size; proc++) { + size_t p_start = (proc * rows_per_process) + std::min(proc, remainder); + size_t p_end = p_start + rows_per_process + (proc < remainder ? 1 : 0); + size_t p_rows = p_end - p_start; + size_t p_data_size = output.width * p_rows * output.channels; + + std::vector recv_buffer(p_data_size); + MPI_Recv(recv_buffer.data(), static_cast(p_data_size), MPI_UNSIGNED_CHAR, static_cast(proc), 0, + MPI_COMM_WORLD, MPI_STATUS_IGNORE); + + for (size_t row = 0; row < p_rows; row++) { + for (size_t col = 0; col < output.width; col++) { + for (size_t ch = 0; ch < output.channels; ch++) { + size_t output_index = (((p_start + row) * output.width + col) * output.channels) + ch; + size_t recv_buf_index = ((row * output.width + col) * output.channels) + ch; + output.data[output_index] = recv_buffer[recv_buf_index]; + } + } + } + } +} + +} // namespace + +BatkovFLinearImageFilteringMPI::BatkovFLinearImageFilteringMPI(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = Image(); +} + +bool BatkovFLinearImageFilteringMPI::ValidationImpl() { + return (!GetInput().data.empty()) && (GetInput().width > 0) && (GetInput().height > 0); +} + +bool BatkovFLinearImageFilteringMPI::PreProcessingImpl() { + kernel_ = {{1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}, + {2.0F / 16.0F, 4.0F / 16.0F, 2.0F / 16.0F}, + {1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}}; + + return true; +} + +bool BatkovFLinearImageFilteringMPI::RunImpl() { + int int_rank = 0; + int int_size = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &int_rank); + MPI_Comm_size(MPI_COMM_WORLD, &int_size); + + const auto rank = static_cast(int_rank); + const auto size = static_cast(int_size); + + auto &input = GetInput(); + size_t width = input.width; + size_t height = input.height; + size_t channels = input.channels; + + const size_t kernel_size = 3; + const size_t half = kernel_size / 2; + + size_t rows_per_process = height / size; + size_t remainder = height % size; + size_t start_row = (rank * rows_per_process) + std::min(rank, remainder); + size_t end_row = start_row + rows_per_process + (rank < remainder ? 1 : 0); + + size_t local_start = (start_row > half) ? start_row - half : 0; + size_t local_end = (end_row + half < height) ? end_row + half : height; + + Image block; + block.width = width; + block.height = local_end - local_start; + block.channels = channels; + block.data.resize(block.width * block.height * block.channels); + CopyBlockData(input, block, local_start); + + Image result_block; + result_block.width = width; + result_block.height = end_row - start_row; + result_block.channels = channels; + result_block.data.resize(result_block.width * result_block.height * result_block.channels); + ProcessBlock(block, result_block, kernel_, start_row, local_start); + + Image result; + result.width = width; + result.height = height; + result.channels = channels; + result.data.resize(width * height * channels); + + if (rank == 0) { + GatherResultsFromProcesses(size, rows_per_process, remainder, result_block, start_row, result); + } else { + MPI_Send(result_block.data.data(), static_cast(result_block.data.size()), MPI_UNSIGNED_CHAR, 0, 0, + MPI_COMM_WORLD); + } + + MPI_Barrier(MPI_COMM_WORLD); + + MPI_Bcast(result.data.data(), static_cast(result.data.size()), MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD); + GetOutput() = std::move(result); + + MPI_Barrier(MPI_COMM_WORLD); + return true; +} + +bool BatkovFLinearImageFilteringMPI::PostProcessingImpl() { + return true; +} + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/report.md b/tasks/batkov_f_linear_image_filtering/report.md new file mode 100644 index 0000000000..5172cc19c1 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/report.md @@ -0,0 +1,386 @@ +# Линейная фильтрация изображений (блочное разбиение). Ядро Гаусса 3x3. + +- **Студент**: Батьков Филипп Владиславович, группа 3823Б1ПР3 +- **Технология**: SEQ | MPI +- **Вариант**: 28 + +## 1. Введение + +Цель работы — разработать и реализовать линейной фильтраци изображений двумя способами: + +- **последовательная (SEQ)** реализация на одном процессе; +- **параллельная (MPI)** реализация с блочным распараллеливанием изображения. + +Дополнительно необходимо: + +- реализовать вспомогательную инфраструктуру для генерации случайного и детерминированного шума, а также для построения гауссовых ядер; +- реализовать детектор "размытия" изображения на основе дисперсии лапласиана для проверки корректности фильтрации; +- написать функциональные и производительные тесты и сравнить поведение SEQ и MPI реализаций на реальных изображениях. + +## 2. Постановка задачи + +На вход алгоритма подаётся сгенерируемое программно цветное изображение. + +Типы входных и выходных данных: + +```cpp +struct Image { + std::vector data; + size_t width; + size_t height; + size_t channels; +}; + +using InType = Image; // исходное изображение +using OutType = Image; // сглаженное изображение +``` + +Структура `Image` содержит: + +- размеры изображения `width`, `height`; +- количество каналов `channels` (1, 3 или 4); +- вектор байтов `std::vector data`, хранящий пиксели в формате (RGB[A]). + +Задача: по входному изображению построить новое изображение, на которое применён **оператор фильтрации** с фиксированным ядром 3×3. Полученное изображение должно быть более чистым, что проверяется при помощи дисперсии Лапласиана. + +## 3. Базовый алгоритм (последовательная версия) + +Последовательная реализация находится в `seq/src/ops_seq.cpp`, класс `BatkovFLinearImageFilteringSEQ`. + +### 3.1. Формирование гауссова ядра + +Гауссово ядро размером `3×3` задается прямым образом в функции `PreProcessingImpl`: + +```cpp +kernel_ = {{1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}, + {2.0F / 16.0F, 4.0F / 16.0F, 2.0F / 16.0F}, + {1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}}; +``` + +Это классическое дискретное гауссово ядро, нормированное так, чтобы сумма коэффициентов была равна 1. + +### 3.2. Схема свёртки + +Алгоритм последовательного сглаживания (`BatkovFLinearImageFilteringSEQ::RunImpl`) выполняет двумерную свёртку исходного изображения с гауссовым ядром: + +- извлекаются размеры `width`, `height`, количество каналов `channels` и массив пикселей `img_data`; +- создаётся выходной буфер `temp` размером `width * height * channels`; +- для каждого пикселя `(row, col)` и каждого канала `ch` вычисляется новое значение как взвешенная сумма соседних пикселей в окрестности `3×3`: + +```cpp +for (size_t row = 0; row < height; row++) { +for (size_t col = 0; col < width; col++) { + for (size_t ch = 0; ch < channels; ch++) { + float val = ApplyKernel(kernel_, GetInput(), row, col, ch); + size_t index = (((row * width) + col) * channels) + ch; + GetOutput().data[index] = static_cast(std::clamp(val, 0.0F, 255.0F)); + } +} +} +``` + +Границы обрабатываются по схеме **зеркального отражения**: индексы, выходящие за границу, прижимаются к диапазону `[0, width-1]` / `[0, height-1]`. + +## 4. Схема распараллеливания (MPI) + +Параллельная реализация описана в `mpi/src/ops_mpi.cpp`, класс `BatkovFLinearImageFilteringMPI`. + +### 4.1. Общая идея + +Распараллеливание выполняется на блоки с определнным количеством строк: + +- все процессы знают размеры изображения и входные данные (`GetInput()` одинаков на всех рангах); +- по вертикали (`height`) изображение делится на блоки строк между процессами (почти равные по количеству строк, остаток раздается первым процессам); +- каждый процесс обрабатывает свои строки, но для корректной свёртки с ядром 3×3 ему нужны также **дополнительные строки сверху и снизу** ("halo"-область); +- после локальной обработки каждый процесс отправляет свой фрагмент результата процессу 0, который собирает полное изображение и затем рассылает его всем процессам. + +### 4.2. Определение ранга и размеров + +В начале `RunImpl` выполняется стандартная инициализация ранга и количетсва процессов MPI: + +```cpp +int int_rank = 0; +int int_size = 0; +MPI_Comm_rank(MPI_COMM_WORLD, &int_rank); +MPI_Comm_size(MPI_COMM_WORLD, &int_size); + +const auto rank = static_cast(int_rank); +const auto size = static_cast(int_size); + +auto &input = GetInput(); +size_t width = input.width; +size_t height = input.height; +size_t channels = input.channels; +``` + +Затем читаются параметры входного изображения `width`, `height`, `channels` и ссылка на исходные данные. + +### 4.3. Разбиение на блоки + +Количество строк на процесс и диапазон строк для текущего ранга: + +```cpp +size_t rows_per_process = height / size; +size_t remainder = height % size; + +size_t start_row = rank * rows_per_process + std::min(rank, remainder); +size_t end_row = start_row + rows_per_process + (rank < remainder ? 1 : 0); +``` + +Такой подход гарантирует, что первые `remainder` процессов получат на одну строку больше. + +### 4.4. Halo-область для свёртки + +Чтобы корректно посчитать свёртку вблизи горизонтальных границ локального блока, каждый процесс расширяет свой диапазон за счёт соседних строк: + +```cpp +const size_t kernel_size = 3; +const size_t half = kernel_size / 2; + +size_t local_start = (start_row > half) ? start_row - half : 0; +size_t local_end = (end_row + half < height) ? end_row + half : height; +size_t local_height = local_end - local_start; +``` + +### 4.5. Копирование локальной области + +Функция `CopyBlockData` копирует необходимые строки исходного изображения в новый блок: + +```cpp +Image block; +block.width = width; +block.height = local_end - local_start; +block.channels = channels; +block.data.resize(block.width * block.height * block.channels); +CopyBlockData(input, block, local_start); +``` + +Каждая строка `global_row` из диапазона `[local_start, local_end)` копируется в соответствующую строку локального блока. + +### 4.6. Локальная свёртка + +Функция `ProcessBlock` выполняет свёртку только для строк одного блока `[start_row, end_row)`, используя halo-строки из ранее скопированного `block`: + +```cpp +Image result_block; +result_block.width = width; +result_block.height = end_row - start_row; +result_block.channels = channels; +result_block.data.resize(result_block.width * result_block.height * result_block.channels); +ProcessBlock(block, result_block, kernel_, start_row, local_start); +``` + +Внутри функции логика свёртки полностью аналогична последовательной версии, но индексы `row_in_block` сдвинуты на `local_start`. + +### 4.7. Сборка результата на процессе 0 + +Процесс 0 собирает фрагменты от всех процессов с помощью вспомогательных функций: + +```cpp +Image result; +result.width = width; +result.height = height; +result.channels = channels; +result.data.resize(width * height * channels); + +if (rank == 0) { + GatherResultsFromProcesses(size, rows_per_process, remainder, result_block, start_row, result); +} else { + MPI_Send(result_block.data.data(), static_cast(result_block.data.size()), MPI_UNSIGNED_CHAR, 0, 0, + MPI_COMM_WORLD); +} +``` + +Функция `GatherResultsFromProcesses` на процессе 0: + +- сначала копирует локальный блок процесса 0 в `result` (`CopyBlockToOutput`); +- затем в цикле по `proc = 1 .. size-1` принимает фрагменты через `MPI_Recv` и раскладывает их на нужные позиции в `result`. + +### 4.8. Рассылка результата всем процессам + +Чтобы функциональные тесты могли проверять результат на каждом процессе, итоговое изображение рассылается всем рангам через `MPI_Bcast`: + +```cpp +MPI_Barrier(MPI_COMM_WORLD); +MPI_Bcast(result.data.data(), static_cast(result.data.size()), MPI_UNSIGNED_CHAR, 0, MPI_COMM_WORLD); +GetOutput() = std::move(result); +MPI_Barrier(MPI_COMM_WORLD); +``` + +Таким образом, после окончания `RunImpl` **все процессы** содержат идентичный результат сглаживания в `GetOutput()`. + +## 5. Детали реализации и структура проекта + +Структура каталога задачи: + +```text +tasks/batkov_f_batkov_f_linear_image_filteringimage_smoothing/ +├── common +│ └── include +│ └── common.hpp +├── info.json +├── mpi +│ ├── include +│ │ └── ops_mpi.hpp +│ └── src +│ └── ops_mpi.cpp +├── seq +│ ├── include +│ │ └── ops_seq.hpp +│ └── src +│ └── ops_seq.cpp +├── settings.json +└── tests + ├── functional + │ └── main.cpp + └── performance + └── main.cpp +``` + +Основные классы: + +- `BatkovFLinearImageFilteringSEQ` — последовательная реализация; +- `BatkovFLinearImageFilteringMPI` — MPI-реализация с блочным разбиением; +- `BatkovFRunFuncTestsProcesses3` — функциональные тесты (сравнение с детектором сглаженности); +- `BatkovFRunPerfTestProcesses3` — тесты производительности. + +Функция `CalcLaplacianVariance` используется в обоих тестах для проверки, что изображение действительно стало более чистым. + +```cpp +static float CalcLaplacianVariance(const Image& image) +{ + std::vector gray(image.width * image.height); + + const auto &data = image.data; + size_t width = image.width; + size_t height = image.height; + size_t channels = image.channels; + + if (channels == 1) { + for (size_t i = 0; i < width * height; i++) { + gray[i] = static_cast(data[i]); + } + } else { + for (size_t i = 0; i < width * height; i++) { + size_t idx = i * channels; + auto r = static_cast(data[idx + 0]); + auto g = static_cast(data[idx + 1]); + auto b = static_cast(data[idx + 2]); + + gray[i] = (0.299F * r) + (0.587F * g) + (0.114F * b); + } + } + + std::vector laplacian(width * height, 0.0F); + for (size_t y_px = 1; y_px < height - 1; y_px++) { + for (size_t x_px = 1; x_px < width - 1; x_px++) { + size_t idx = (y_px * width) + x_px; + + float value = -gray[((y_px - 1) * width) + x_px] - gray[(y_px * width) + (x_px - 1)] + (4.0F * gray[idx]) - + gray[(y_px * width) + (x_px + 1)] - gray[((y_px + 1) * width) + x_px]; + + laplacian[idx] = value; + } + } + + float mean = 0.0F; + for (size_t i = 0; i < width * height; i++) { + mean += laplacian[i]; + } + mean /= static_cast(width * height); + + float variance = 0.0F; + for (size_t i = 0; i < width * height; i++) { + float diff = laplacian[i] - mean; + variance += diff * diff; + } + variance /= static_cast(width * height); + + return variance; +} +``` + +Алгоритм: + +1. Перевод изображения в **градации серого** либо берётся единственный канал, либо используется стандартная формула (Y = 0.299 R + 0.587 G + 0.114 B). +2. Вычисление **лапласиана** в каждой внутренней точке (5-точечный шаблон): + +```cpp +float value = -gray[(y-1)*width + x] - gray[y*width + (x-1)] + + 4.0F * gray[idx] + - gray[y*width + (x+1)] - gray[(y+1)*width + x]; +``` + +3. Подсчёт средней и дисперсии значений лапласиана по всему изображению. + +## 6. Экспериментальная среда + +| Компонент | Значение | +|-----------|----------------------------------------| +| CPU | Apple M2 (8 ядер) | +| RAM | 16 GB | +| ОС | macOS 15.3.1 | +| Компилятор| g++ (через CMake), стандарт C++20 | +| MPI | mpirun (Open MPI) 5.0.8 | + +Тестовые данные: + +1. **Функциональные тесты** (`tests/functional/main.cpp`): + - изображения с шумом разного размера генерируются случайным образом; + - для каждого изображения задаётся начальное значение размытия `preprocess_blur_value_`; + - для каждого теста запускаются обе реализации: SEQ и MPI; + - в конце еще раз подсчитывается значение размытия и сравнивается с начальным. + +2. **Тесты производительности** (`tests/performance/main.cpp`): + - изображение с детерминированным шумом генерируется на основе хеша; + - тестовый фреймворк `BaseRunPerfTests` автоматически прогоняет SEQ и MPI-версии в различных режимах запуска (в т.ч. `task_run` и `pipeline`) и для разного числа процессов. + +## 7. Результаты и обсуждение + +### 7.1. Корректность + +- Функциональные тесты проверяют, что результат сглаживания удовлетворяет критерию `(preprocess_blur_value_ / post_process_blur_value) > 2.0F`. +- Для каждого тестового изображения по результатам работы SEQ и MPI реализаций детектор размытия выдаёт одинаковый ответ, что говорит о **функциональной эквивалентности** алгоритмов. + +### 7.2. Производительность + +**pipeline:** + +| Mode | Count | Time, s | Speedup | Efficiency | +| ---- | ----- | ------- | ------- | ---------- | +| SEQ | 1 | 0.3637 | 1.00 | N/A | +| MPI | 1 | 0.4688 | 0.78 | 78.0% | +| MPI | 2 | 0.2799 | 1.30 | 65.0% | +| MPI | 4 | 0.1823 | 1.99 | 49.8% | +| MPI | 8 | 0.2522 | 1.44 | 18.0% | + +**task_run:** + +| Mode | Count | Time, s | Speedup | Efficiency | +| ---- | ----- | ------- | ------- | ---------- | +| SEQ | 1 | 0.3629 | 1.00 | N/A | +| MPI | 1 | 0.4571 | 0.79 | 79.3% | +| MPI | 2 | 0.2742 | 1.32 | 66.2% | +| MPI | 4 | 0.1830 | 1.98 | 49.6% | +| MPI | 8 | 0.3299 | 1.10 | 13.7% | + +- При запуске на одном процессе MPI-реализация ожидаемо медленнее SEQ-за счёт накладных расходов на инициализацию MPI и обмен данными. +- При увеличении числа процессов наблюдается уменьшение времени выполнения MPI-версии до определённого предела: строковое разбиение хорошо масштабируется по числу процессов, пока +коммуникационные расходы не начинают преобладать. +- Эффективность распараллеливания сильно зависит от размеров изображения: чем больше пикселей обрабатывает каждый процесс, тем лучше соотношение "вычисления/коммуникации". + +## 8. Заключение + +В рамках работы реализованы: + +1. **Последовательный алгоритм** гауссова сглаживания изображения с ядром 5×5, корректно обрабатывающий границы кадра. +2. **Параллельная MPI-реализация**, использующая разбиение по строкам и halo-область для точного воспроизведения результата свёртки на каждом процессе. +3. **Функциональные и производительные тесты**, демонстрирующие корректность и исследующие поведение алгоритма при разных режимах запуска. + +MPI-реализация показывает выигрыш по времени при достаточно больших изображениях и числе процессов, однако эффект ограничивается ростом накладных расходов на синхронизацию и передачу блоков изображения. Тем не менее, предложенный подход легко масштабируется и может быть расширен для более сложных фильтров (большие ядра, последовательность нескольких свёрток) и трёхмерных изображений. + +## 10. Источники + +1. [Материалы курса](https://learning-process.github.io/parallel_programming_course/ru/common_information/report.html) +2. [Документация Open MPI](https://www.open-mpi.org/doc/) +3. [MPI стандарт](https://www.mpi-forum.org/) diff --git a/tasks/batkov_f_linear_image_filtering/seq/include/ops_seq.hpp b/tasks/batkov_f_linear_image_filtering/seq/include/ops_seq.hpp new file mode 100644 index 0000000000..3ae8a915f1 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/seq/include/ops_seq.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" +#include "task/include/task.hpp" + +namespace batkov_f_linear_image_filtering { + +class BatkovFLinearImageFilteringSEQ : public BaseTask { + public: + static constexpr ppc::task::TypeOfTask GetStaticTypeOfTask() { + return ppc::task::TypeOfTask::kSEQ; + } + explicit BatkovFLinearImageFilteringSEQ(const InType &in); + + private: + bool ValidationImpl() override; + bool PreProcessingImpl() override; + bool RunImpl() override; + bool PostProcessingImpl() override; + + Kernel kernel_; +}; + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/seq/src/ops_seq.cpp b/tasks/batkov_f_linear_image_filtering/seq/src/ops_seq.cpp new file mode 100644 index 0000000000..830763b716 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/seq/src/ops_seq.cpp @@ -0,0 +1,85 @@ +#include "batkov_f_linear_image_filtering/seq/include/ops_seq.hpp" + +#include +#include +#include + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" + +namespace batkov_f_linear_image_filtering { + +namespace { + +float ApplyKernel(const Kernel &kernel, const Image &image, size_t row, size_t col, size_t ch) { + const size_t width = image.width; + const size_t height = image.height; + const size_t channels = image.channels; + + float sum = 0.0F; + + for (size_t ky = 0; ky < 3; ky++) { + for (size_t kx = 0; kx < 3; kx++) { + size_t py = row + ky - 1; + size_t px = col + kx - 1; + + py = std::max(py, 0); + py = std::min(py, height - 1); + px = std::max(px, 0); + px = std::min(px, width - 1); + + auto index = (((py * width) + px) * channels) + ch; + sum += static_cast(image.data[index]) * kernel[ky][kx]; + } + } + + return sum; +} + +} // namespace + +BatkovFLinearImageFilteringSEQ::BatkovFLinearImageFilteringSEQ(const InType &in) { + SetTypeOfTask(GetStaticTypeOfTask()); + GetInput() = in; + GetOutput() = Image{}; +} + +bool BatkovFLinearImageFilteringSEQ::ValidationImpl() { + return (!GetInput().data.empty()) && (GetInput().width > 0) && (GetInput().height > 0); +} + +bool BatkovFLinearImageFilteringSEQ::PreProcessingImpl() { + kernel_ = {{1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}, + {2.0F / 16.0F, 4.0F / 16.0F, 2.0F / 16.0F}, + {1.0F / 16.0F, 2.0F / 16.0F, 1.0F / 16.0F}}; + + GetOutput().width = GetInput().width; + GetOutput().height = GetInput().height; + GetInput().channels = GetInput().channels; + GetOutput().data.resize(GetInput().width * GetInput().height * GetInput().channels); + + return true; +} + +bool BatkovFLinearImageFilteringSEQ::RunImpl() { + size_t width = GetInput().width; + size_t height = GetInput().height; + size_t channels = GetInput().channels; + + for (size_t row = 0; row < height; row++) { + for (size_t col = 0; col < width; col++) { + for (size_t ch = 0; ch < channels; ch++) { + float val = ApplyKernel(kernel_, GetInput(), row, col, ch); + size_t index = (((row * width) + col) * channels) + ch; + GetOutput().data[index] = static_cast(std::clamp(val, 0.0F, 255.0F)); + } + } + } + + return true; +} + +bool BatkovFLinearImageFilteringSEQ::PostProcessingImpl() { + return true; +} + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/settings.json b/tasks/batkov_f_linear_image_filtering/settings.json new file mode 100644 index 0000000000..b1a0d52574 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/settings.json @@ -0,0 +1,7 @@ +{ + "tasks_type": "processes", + "tasks": { + "mpi": "enabled", + "seq": "enabled" + } +} diff --git a/tasks/batkov_f_linear_image_filtering/tests/.clang-tidy b/tasks/batkov_f_linear_image_filtering/tests/.clang-tidy new file mode 100644 index 0000000000..ef43b7aa8a --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/tests/.clang-tidy @@ -0,0 +1,13 @@ +InheritParentConfig: true + +Checks: > + -modernize-loop-convert, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-non-const-global-variables, + -misc-use-anonymous-namespace, + -modernize-use-std-print, + -modernize-type-traits + +CheckOptions: + - key: readability-function-cognitive-complexity.Threshold + value: 50 # Relaxed for tests diff --git a/tasks/batkov_f_linear_image_filtering/tests/functional/main.cpp b/tasks/batkov_f_linear_image_filtering/tests/functional/main.cpp new file mode 100644 index 0000000000..d6398b0675 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/tests/functional/main.cpp @@ -0,0 +1,138 @@ +#include + +#include +#include +#include +#include +#include +#include + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" +#include "batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp" +#include "batkov_f_linear_image_filtering/seq/include/ops_seq.hpp" +#include "util/include/func_test_util.hpp" +#include "util/include/util.hpp" + +namespace batkov_f_linear_image_filtering { + +class BatkovFRunFuncTestsProcesses3 : public ppc::util::BaseRunFuncTests { + public: + static std::string PrintTestParam(const TestType &test_param) { + std::string p0 = std::get<0>(test_param); + std::string p1 = std::to_string(std::get<1>(test_param)); + std::string p2 = std::to_string(std::get<2>(test_param)); + std::string p3 = std::to_string(std::get<3>(test_param)); + return p0 + "_" + p1 + "x" + p2 + "x" + p3; + } + + protected: + void SetUp() override { + TestType params = std::get(ppc::util::GTestParamIndex::kTestParams)>(GetParam()); + input_data_.width = std::get<1>(params); + input_data_.height = std::get<2>(params); + input_data_.channels = std::get<3>(params); + + size_t size = input_data_.width * input_data_.height * input_data_.channels; + input_data_.data.resize(size); + + for (size_t i = 0; i < size; ++i) { + input_data_.data[i] = dis_(gen_); + } + + preprocess_blur_value_ = CalcLaplacianVariance(input_data_); + } + + static float CalcLaplacianVariance(const Image &image) { + std::vector gray(image.width * image.height); + + const auto &data = image.data; + size_t width = image.width; + size_t height = image.height; + size_t channels = image.channels; + + if (channels == 1) { + for (size_t i = 0; i < width * height; i++) { + gray[i] = static_cast(data[i]); + } + } else { + for (size_t i = 0; i < width * height; i++) { + size_t idx = i * channels; + auto r = static_cast(data[idx + 0]); + auto g = static_cast(data[idx + 1]); + auto b = static_cast(data[idx + 2]); + + gray[i] = (0.299F * r) + (0.587F * g) + (0.114F * b); + } + } + + std::vector laplacian(width * height, 0.0F); + for (size_t y_px = 1; y_px < height - 1; y_px++) { + for (size_t x_px = 1; x_px < width - 1; x_px++) { + size_t idx = (y_px * width) + x_px; + + float value = -gray[((y_px - 1) * width) + x_px] - gray[(y_px * width) + (x_px - 1)] + (4.0F * gray[idx]) - + gray[(y_px * width) + (x_px + 1)] - gray[((y_px + 1) * width) + x_px]; + + laplacian[idx] = value; + } + } + + float mean = 0.0F; + for (size_t i = 0; i < width * height; i++) { + mean += laplacian[i]; + } + mean /= static_cast(width * height); + + float variance = 0.0F; + for (size_t i = 0; i < width * height; i++) { + float diff = laplacian[i] - mean; + variance += diff * diff; + } + variance /= static_cast(width * height); + + return variance; + } + + bool CheckTestOutputData(OutType &output_data) final { + float post_process_blur_value = CalcLaplacianVariance(output_data); + + return (preprocess_blur_value_ / post_process_blur_value) > 2.0F; + } + + InType GetTestInputData() final { + return input_data_; + } + + private: + std::random_device rd_; + std::mt19937 gen_{rd_()}; + std::uniform_int_distribution dis_{0, 255}; + + float preprocess_blur_value_ = 0.0F; + InType input_data_; +}; + +namespace { + +TEST_P(BatkovFRunFuncTestsProcesses3, LinearFiltering) { + ExecuteTest(GetParam()); +} + +const std::array kTestParam = { + std::make_tuple("tiny_image", 10, 10, 3), std::make_tuple("small_image", 50, 50, 3), + std::make_tuple("medium_image", 100, 100, 3), std::make_tuple("big_image", 300, 300, 3)}; + +const auto kTestTasksList = std::tuple_cat(ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_batkov_f_linear_image_filtering), + ppc::util::AddFuncTask( + kTestParam, PPC_SETTINGS_batkov_f_linear_image_filtering)); + +const auto kGtestValues = ppc::util::ExpandToValues(kTestTasksList); + +const auto kPerfTestName = BatkovFRunFuncTestsProcesses3::PrintFuncTestName; + +INSTANTIATE_TEST_SUITE_P(LinearFilteringTests, BatkovFRunFuncTestsProcesses3, kGtestValues, kPerfTestName); + +} // namespace + +} // namespace batkov_f_linear_image_filtering diff --git a/tasks/batkov_f_linear_image_filtering/tests/performance/main.cpp b/tasks/batkov_f_linear_image_filtering/tests/performance/main.cpp new file mode 100644 index 0000000000..6ea69f04a9 --- /dev/null +++ b/tasks/batkov_f_linear_image_filtering/tests/performance/main.cpp @@ -0,0 +1,125 @@ +#include + +#include +#include +#include + +#include "batkov_f_linear_image_filtering/common/include/common.hpp" +#include "batkov_f_linear_image_filtering/mpi/include/ops_mpi.hpp" +#include "batkov_f_linear_image_filtering/seq/include/ops_seq.hpp" +#include "util/include/perf_test_util.hpp" + +namespace batkov_f_linear_image_filtering { + +class BatkovFRunPerfTestProcesses3 : public ppc::util::BaseRunPerfTests { + InType input_data_; + float preprocess_blur_value_ = 0.0F; + + void SetUp() override { + size_t width = 7680; + size_t height = 4320; + size_t channels = 3; + + input_data_.width = width; + input_data_.height = height; + input_data_.channels = channels; + + input_data_.data.resize(width * height * channels); + + for (size_t col = 0; col < width; col += channels) { + for (size_t row = 0; row < height; row += channels) { + unsigned int hash_r = 2654435761U ^ (row * 73856093U); + unsigned int hash_g = 2654435761U ^ (col * 19349663U); + unsigned int hash_b = 2654435761U ^ ((row + col) * 83492791U); + + hash_r = (hash_r ^ (hash_r >> 13)) * 2654435761U; + hash_g = (hash_g ^ (hash_g >> 13)) * 2654435761U; + hash_b = (hash_b ^ (hash_b >> 13)) * 2654435761U; + + size_t index = (col * height) + row; + input_data_.data[index + 0] = static_cast((hash_r >> 8) & 0xFF); + input_data_.data[index + 1] = static_cast((hash_g >> 8) & 0xFF); + input_data_.data[index + 2] = static_cast((hash_b >> 8) & 0xFF); + } + } + + preprocess_blur_value_ = CalcLaplacianVariance(input_data_); + } + + static float CalcLaplacianVariance(const Image &image) { + std::vector gray(image.width * image.height); + + const auto &data = image.data; + size_t width = image.width; + size_t height = image.height; + size_t channels = image.channels; + + if (channels == 1) { + for (size_t i = 0; i < width * height; i++) { + gray[i] = static_cast(data[i]); + } + } else { + for (size_t i = 0; i < width * height; i++) { + size_t idx = i * channels; + auto r = static_cast(data[idx + 0]); + auto g = static_cast(data[idx + 1]); + auto b = static_cast(data[idx + 2]); + + gray[i] = (0.299F * r) + (0.587F * g) + (0.114F * b); + } + } + + std::vector laplacian(width * height, 0.0F); + for (size_t y_px = 1; y_px < height - 1; y_px++) { + for (size_t x_px = 1; x_px < width - 1; x_px++) { + size_t idx = (y_px * width) + x_px; + + float value = -gray[((y_px - 1) * width) + x_px] - gray[(y_px * width) + (x_px - 1)] + (4.0F * gray[idx]) - + gray[(y_px * width) + (x_px + 1)] - gray[((y_px + 1) * width) + x_px]; + + laplacian[idx] = value; + } + } + + float mean = 0.0F; + for (size_t i = 0; i < width * height; i++) { + mean += laplacian[i]; + } + mean /= static_cast(width * height); + + float variance = 0.0F; + for (size_t i = 0; i < width * height; i++) { + float diff = laplacian[i] - mean; + variance += diff * diff; + } + variance /= static_cast(width * height); + + return variance; + } + + bool CheckTestOutputData(OutType &output_data) final { + float post_process_blur_value = CalcLaplacianVariance(output_data); + + return (preprocess_blur_value_ / post_process_blur_value) > 2.0F; + } + + InType GetTestInputData() final { + return input_data_; + } +}; + +TEST_P(BatkovFRunPerfTestProcesses3, RunPerfModes) { + ExecuteTest(GetParam()); +} + +const auto kAllPerfTasks = + ppc::util::MakeAllPerfTasks( + PPC_SETTINGS_batkov_f_linear_image_filtering); + +const auto kGtestValues = ppc::util::TupleToGTestValues(kAllPerfTasks); + +const auto kPerfTestName = BatkovFRunPerfTestProcesses3::CustomPerfTestName; + +INSTANTIATE_TEST_SUITE_P(RunModeTests, BatkovFRunPerfTestProcesses3, kGtestValues, kPerfTestName); + +} // namespace batkov_f_linear_image_filtering