diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..4cba627 --- /dev/null +++ b/.clang-format @@ -0,0 +1,23 @@ +BasedOnStyle: LLVM +Language: Cpp + +IndentWidth: 2 +ContinuationIndentWidth: 2 +TabWidth: 2 +UseTab: Never + +ColumnLimit: 80 +IndentPPDirectives: None +AlignEscapedNewlines: Left +AlignConsecutiveMacros: None + +BinPackArguments: false +BinPackParameters: false +AllowShortFunctionsOnASingleLine: Empty +AllowShortIfStatementsOnASingleLine: Never +AllowShortLoopsOnASingleLine: false + +BreakBeforeBraces: Attach +PointerAlignment: Right +SortIncludes: Never +ReflowComments: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d68dbb --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.cache +/build +__pycache__/ +*.pyc +/benchmarks/processed_outputs +/benchmarks/pipeline_outputs +/benchmarks/results \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0b1d067 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,131 @@ +cmake_minimum_required(VERSION 3.20) + +project(ImageConversation LANGUAGES C) + +set(CMAKE_C_STANDARD 17) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(CTest) +include(FetchContent) + +option(IMAGE_CONVERSATION_ENABLE_OPENCV + "Fetch the legacy OpenCV C API target" + OFF) + +file(GLOB_RECURSE FILTER_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/filters/*.c" +) + +file(GLOB_RECURSE CONVOLUTION_RUNTIME_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/src/sequentially_convolution/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/image_helpers/*.c" +) + +set(BUILD_TESTS OFF CACHE BOOL "") +set(BUILD_PERF_TESTS OFF CACHE BOOL "") +set(BUILD_EXAMPLES OFF CACHE BOOL "") +set(BUILD_DOCS OFF CACHE BOOL "") +set(BUILD_opencv_apps OFF CACHE BOOL "") + +set(BUILD_opencv_core ON CACHE BOOL "") +set(BUILD_opencv_imgproc ON CACHE BOOL "") +set(BUILD_opencv_highgui ON CACHE BOOL "") + +set(BUILD_opencv_ts OFF CACHE BOOL "") +set(BUILD_opencv_calib3d OFF CACHE BOOL "") +set(BUILD_opencv_contrib OFF CACHE BOOL "") +set(BUILD_opencv_features2d OFF CACHE BOOL "") +set(BUILD_opencv_flann OFF CACHE BOOL "") +set(BUILD_opencv_gpu OFF CACHE BOOL "") +set(BUILD_opencv_legacy OFF CACHE BOOL "") +set(BUILD_opencv_ml OFF CACHE BOOL "") +set(BUILD_opencv_nonfree OFF CACHE BOOL "") +set(BUILD_opencv_objdetect OFF CACHE BOOL "") +set(BUILD_opencv_ocl OFF CACHE BOOL "") +set(BUILD_opencv_photo OFF CACHE BOOL "") +set(BUILD_opencv_stitching OFF CACHE BOOL "") +set(BUILD_opencv_superres OFF CACHE BOOL "") +set(BUILD_opencv_video OFF CACHE BOOL "") +set(BUILD_opencv_videostab OFF CACHE BOOL "") +set(BUILD_opencv_world OFF CACHE BOOL "") + +set(WITH_QT OFF CACHE BOOL "") +set(WITH_GTK OFF CACHE BOOL "") +set(WITH_OPENGL OFF CACHE BOOL "") +set(WITH_FFMPEG OFF CACHE BOOL "") +set(WITH_GSTREAMER OFF CACHE BOOL "") +set(WITH_V4L OFF CACHE BOOL "") +set(WITH_1394 OFF CACHE BOOL "") +set(WITH_CUDA OFF CACHE BOOL "") +set(WITH_OPENCL OFF CACHE BOOL "") +set(WITH_TBB OFF CACHE BOOL "") +set(WITH_IPP OFF CACHE BOOL "") +set(WITH_TIFF OFF CACHE BOOL "") +set(WITH_JASPER OFF CACHE BOOL "") +set(WITH_OPENEXR OFF CACHE BOOL "") + +FetchContent_Declare( + opencv + GIT_REPOSITORY https://github.com/opencv/opencv + GIT_TAG 2.4.13.6 + GIT_SHALLOW TRUE +) + +FetchContent_MakeAvailable(opencv) + +add_library(opencv_legacy_c_api INTERFACE) +target_include_directories(opencv_legacy_c_api + INTERFACE + ${opencv_SOURCE_DIR}/modules/core/include + ${opencv_SOURCE_DIR}/modules/imgproc/include + ${opencv_SOURCE_DIR}/modules/highgui/include +) +target_link_libraries(opencv_legacy_c_api + INTERFACE + opencv_core + opencv_imgproc + opencv_highgui +) + +add_library(filters_core STATIC + ${FILTER_SOURCES} +) +add_library(filters ALIAS filters_core) + +target_include_directories(filters_core + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +add_library(convolution_runtime STATIC + ${CONVOLUTION_RUNTIME_SOURCES} +) +target_include_directories(convolution_runtime + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/internal +) +target_link_libraries(convolution_runtime + PUBLIC + filters_core + opencv_legacy_c_api +) + +add_executable(app + src/app.c + src/cli_args.c +) +target_link_libraries(app + PRIVATE + convolution_runtime +) + +if(BUILD_TESTING) + find_package(PkgConfig REQUIRED) + pkg_check_modules(CMOCKA REQUIRED IMPORTED_TARGET cmocka) + + add_subdirectory(test/sequential_tests) +endif() diff --git a/README.md b/README.md index e62fdfb..4871a39 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,32 @@ -# Image Conversation +# Image Conversation — Sequential Convolution -Основная ветка намеренно оставлена минимальной. +Ветка содержит первую задачу: последовательную свёртку одного изображения. -Реализации задач разнесены по отдельным веткам: +В составе ветки оставлены фильтры, загрузка/сохранение изображения, CLI, тестовые изображения и тесты последовательной реализации. Параллельная свёртка, thread pool и pipeline здесь намеренно отсутствуют, чтобы diff относительно `main` показывал только первую задачу. -- `feat/sequential_convolution` — последовательная свёртка. -- `feat/parallel_convolution` — параллельная свёртка поверх последовательной реализации. -- `feat/image_pipepline` — pipeline обработки изображений поверх параллельной реализации. +## Сборка + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF +cmake --build build --target app -j +``` + +## Пример запуска + +```bash +./build/app -i input/sea.png -o output/sea.png -f gauss -h 5 -w 5 -s +``` + +## Посмотреть параметры запуска + +```bash +./build/app --help +``` + +## Тесты + +```bash +cmake -S . -B build-tests -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTING=ON +cmake --build build-tests -j +ctest --test-dir build-tests --output-on-failure +``` diff --git a/docs/task1/README.md b/docs/task1/README.md new file mode 100644 index 0000000..4c91716 --- /dev/null +++ b/docs/task1/README.md @@ -0,0 +1,29 @@ +# Бенчмарк времени обработки изображений + +![График зависимости времени обработки от размера изображения](benchmark.png) + +На графике показана зависимость времени обработки от размера изображения для композиции фильтров `motion_blur 9x9 + gauss 5x5`. Использовались четыре изображения: `satoru`, `stariy_bog`, `musashi`, `sea`. + +По горизонтали отложен размер изображения, по вертикали - среднее время обработки в `мс`. Чем выше точка на графике, тем дольше обрабатывалось изображение. Оранжевые интервалы показывают погрешность среднего значения времени. + +В каждом измерении учитывался полный цикл обработки одного изображения: + +1. чтение изображения; +2. применение `motion_blur 9x9`; +3. применение `gauss 5x5`; +4. запись результата. + +Для каждого изображения выполнено по 10 измерений. В таблице указано среднее время полного цикла обработки и погрешность. + +Погрешность на графике и в таблице - половина 95% доверительного интервала среднего значения времени: + +`error = t_0.975,9 * s / sqrt(n)`, где `n = 10`, `t_0.975,9 = 2.262`, `s` - выборочное стандартное отклонение 10 значений времени. + +| Изображение | Размер | Время, мс | +|---|---:|---:| +| `satoru` | `225x225` | `44.66 +- 0.58` | +| `stariy_bog` | `914x480` | `383.81 +- 0.40` | +| `musashi` | `2560x1440` | `3188.37 +- 9.60` | +| `sea` | `3840x2160` | `7246.61 +- 10.61` | + +Время обработки растёт вместе с размером изображения: минимальное среднее время получилось на `satoru` размером `225x225`, максимальное - на `sea` размером `3840x2160`. diff --git a/docs/task1/benchmark.png b/docs/task1/benchmark.png new file mode 100644 index 0000000..f3faad1 Binary files /dev/null and b/docs/task1/benchmark.png differ diff --git a/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg b/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg new file mode 100644 index 0000000..9c2ce56 Binary files /dev/null and b/input/171406-odin_udar_chelovek_dvojnoe_proniknovenie-prefektura_sajtama-odin_udar_chelovek-anime-rukav-3840x2160.jpg differ diff --git a/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png b/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png new file mode 100644 index 0000000..538e2a1 Binary files /dev/null and b/input/172836-ikona-asfalt-dorozhnoe_pokrytie-most-zdanie-3840x2160.png differ diff --git a/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg b/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg new file mode 100644 index 0000000..d7976ee Binary files /dev/null and b/input/173960-galaktika_andromedy-galaktika-mlechnyj_put-zemlya-zvezda-3840x2160.jpg differ diff --git a/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg b/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg new file mode 100644 index 0000000..4a37dfe Binary files /dev/null and b/input/175978-yastreb-hishhnaya_ptica-sokol-nauka-biologiya-3840x2160.jpg differ diff --git a/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg b/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg new file mode 100644 index 0000000..99b9f81 Binary files /dev/null and b/input/176372-oblako-gora-voda-rastenie-prirodnyj_landshaft-3840x2160.jpg differ diff --git a/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg b/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg new file mode 100644 index 0000000..5c8e462 Binary files /dev/null and b/input/176515-samolet-samolety-polet-reaktivnyj_samolet-aviaciya-3840x2160.jpg differ diff --git a/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg b/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg new file mode 100644 index 0000000..9217e87 Binary files /dev/null and b/input/176585-albert_ejnshtejn_iskusstvo-art-poster-dizajn-nauka-3840x2160.jpg differ diff --git a/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg b/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg new file mode 100644 index 0000000..a1dc170 Binary files /dev/null and b/input/179158-kon-belye-pechen-nazemnye_zhivotnye-prirodnyj_landshaft-3840x2160.jpg differ diff --git a/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg b/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg new file mode 100644 index 0000000..d30a817 Binary files /dev/null and b/input/181366-nytol_herbal_30_tabletok-nosok-bunionektomiya-dostupnyj-prigonka-3840x2160.jpg differ diff --git a/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg b/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg new file mode 100644 index 0000000..45e05a2 Binary files /dev/null and b/input/img3.akspic.ru-nebo-kosmicheskoe_prostranstvo-film-gorizont-atmosfera-3840x2160.jpg differ diff --git a/input/musashi.jpg b/input/musashi.jpg new file mode 100644 index 0000000..5f2320c Binary files /dev/null and b/input/musashi.jpg differ diff --git a/input/satoru.jpg b/input/satoru.jpg new file mode 100644 index 0000000..20c56ff Binary files /dev/null and b/input/satoru.jpg differ diff --git a/input/sea.png b/input/sea.png new file mode 100644 index 0000000..50ddda1 Binary files /dev/null and b/input/sea.png differ diff --git a/input/stariy_bog.png b/input/stariy_bog.png new file mode 100644 index 0000000..a8df030 Binary files /dev/null and b/input/stariy_bog.png differ diff --git a/input/sunshine.jpg b/input/sunshine.jpg new file mode 100644 index 0000000..7fc4963 Binary files /dev/null and b/input/sunshine.jpg differ diff --git a/internal/sequentially_convolution/helper_functions.h b/internal/sequentially_convolution/helper_functions.h new file mode 100644 index 0000000..6c9a653 --- /dev/null +++ b/internal/sequentially_convolution/helper_functions.h @@ -0,0 +1,64 @@ +#pragma once + +#include + +#include + +static inline unsigned char clamp_to_u8(double value) { + if (value < 0.0) { + return 0U; + } + if (value > 255.0) { + return 255U; + } + return (unsigned char)(value + 0.5); +} + +static inline size_t wrap_index(long value, size_t limit) { + long wrapped = value % (long)limit; + if (wrapped < 0) { + wrapped += (long)limit; + } + return (size_t)wrapped; +} + +static inline size_t clamp_index(long value, size_t limit) { + if (value < 0) { + return 0U; + } + if ((size_t)value >= limit) { + return limit - 1U; + } + return (size_t)value; +} + +static inline size_t reflect_index(long value, size_t limit) { + if (limit <= 1U) { + return 0U; + } + + const long period = 2L * ((long)limit - 1L); + long reflected = value % period; + + if (reflected < 0) { + reflected += period; + } + if (reflected >= (long)limit) { + reflected = period - reflected; + } + + return (size_t)reflected; +} + +static inline size_t +resolve_index(long value, size_t limit, filter_border_mode_t border_mode) { + switch (border_mode) { + case FILTER_BORDER_CLAMP: + return clamp_index(value, limit); + case FILTER_BORDER_REFLECT: + return reflect_index(value, limit); + case FILTER_BORDER_WRAP: + default: + return wrap_index(value, limit); + } +} diff --git a/output/.gitkeep b/output/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app.c b/src/app.c new file mode 100644 index 0000000..0ced5e9 --- /dev/null +++ b/src/app.c @@ -0,0 +1,81 @@ +#include "cli_args.h" + +#include +#include + +#include + +#include +#include + +#define MAX_ERROR_MESSAGE_LENGTH 32 + +static int apply_filters(const cli_request_t *request, + image_view_t *image_view) { + for (size_t i = 0; i < request->filter_count; ++i) { + filter_t filter; + filter_request_t filter_request = { + .kind = request->filters[i].kind, + .width = request->filters[i].width, + .height = request->filters[i].height, + .direction = request->filters[i].direction, + .border_mode = request->filters[i].border_mode, + }; + + if (filter_init_builtin(&filter, &filter_request) != FILTER_STATUS_OK) { + return -1; + } + + if (!filter_is_convolution(&filter) || + sequential_convolution(&filter, image_view) != 0) { + return -1; + } + } + + return 0; +} + +int main(int argc, char **argv) { + cli_request_t request; + char error_message[MAX_ERROR_MESSAGE_LENGTH]; + cli_parse_status_t parse_status = + cli_parse_args(argc, argv, &request, error_message, sizeof(error_message)); + + if (parse_status == CLI_PARSE_HELP) { + return 0; + } + + if (parse_status != CLI_PARSE_OK) { + puts(error_message); + return -1; + } + + IplImage *image = cvLoadImage(request.input_path, CV_LOAD_IMAGE_UNCHANGED); + if (image == NULL) { + fprintf(stderr, "failed to load image: %s\n", request.input_path); + return -1; + } + + image_view_t image_view = { + .data = (unsigned char *)image->imageData, + .height = (size_t)image->height, + .width = (size_t)image->width, + .stride = (size_t)image->widthStep, + .channels = (size_t)image->nChannels, + }; + + if (apply_filters(&request, &image_view) != 0) { + cvReleaseImage(&image); + fputs("failed to apply filters\n", stderr); + return -1; + } + + if (!cvSaveImage(request.output_path, image, NULL)) { + cvReleaseImage(&image); + fprintf(stderr, "failed to save image: %s\n", request.output_path); + return -1; + } + + cvReleaseImage(&image); + return 0; +} diff --git a/src/cli_args.c b/src/cli_args.c new file mode 100644 index 0000000..8ec4038 --- /dev/null +++ b/src/cli_args.c @@ -0,0 +1,196 @@ +#include "cli_args.h" + +#include +#include +#include +#include + +#define MIN_ARG_COUNT 12 + +static void cli_set_invalid(char *error_message, size_t error_message_size) { + if (error_message != NULL && error_message_size != 0U) { + snprintf(error_message, error_message_size, "invalid"); + } +} + +static cli_parse_status_t cli_invalid(char *error_message, + size_t error_message_size) { + cli_set_invalid(error_message, error_message_size); + return CLI_PARSE_ERROR; +} + +static bool cli_parse_size(const char *text, size_t *value) { + char *end = NULL; + unsigned long parsed = 0; + + if (text == NULL || value == NULL || *text == '\0') { + return false; + } + + parsed = strtoul(text, &end, 10); + if (*end != '\0' || parsed == 0UL) { + return false; + } + + *value = (size_t)parsed; + return true; +} + +static bool cli_parse_filter(const char *text, filter_kind_t *kind) { + if (strcmp(text, "blur") == 0) { + *kind = FILTER_KIND_BLUR; + } else if (strcmp(text, "mean") == 0) { + *kind = FILTER_KIND_MEAN; + } else if (strcmp(text, "gauss") == 0) { + *kind = FILTER_KIND_GAUSSIAN_BLUR; + } else if (strcmp(text, "motion") == 0) { + *kind = FILTER_KIND_MOTION_BLUR; + } else if (strcmp(text, "edge") == 0) { + *kind = FILTER_KIND_EDGE_DETECT; + } else if (strcmp(text, "sharpen") == 0) { + *kind = FILTER_KIND_SHARPEN; + } else if (strcmp(text, "emboss") == 0) { + *kind = FILTER_KIND_EMBOSS; + } else if (strcmp(text, "median") == 0) { + *kind = FILTER_KIND_MEDIAN; + } else { + return false; + } + + return true; +} + +static bool cli_parse_type(const char *text, filter_direction_t *direction) { + if (strcmp(text, "horizontal") == 0) { + *direction = FILTER_DIRECTION_HORIZONTAL; + } else if (strcmp(text, "vertical") == 0) { + *direction = FILTER_DIRECTION_VERTICAL; + } else if (strcmp(text, "diagonal") == 0) { + *direction = FILTER_DIRECTION_DIAGONAL_45; + } else if (strcmp(text, "omni") == 0) { + *direction = FILTER_DIRECTION_OMNIDIRECTIONAL; + } else { + return false; + } + + return true; +} + +static void cli_init_request(cli_request_t *request) { + memset(request, 0, sizeof(*request)); + for (size_t i = 0; i < CLI_MAX_FILTERS; ++i) { + request->filters[i].direction = FILTER_DIRECTION_NONE; + request->filters[i].border_mode = FILTER_BORDER_WRAP; + } +} + +static bool +cli_parse_io_paths(int argc, char **argv, int *index, cli_request_t *request) { + if (*index + 3 >= argc) { + return false; + } + + if (strcmp(argv[*index], "-i") != 0 || strcmp(argv[*index + 2], "-o") != 0) { + return false; + } + + request->input_path = argv[*index + 1]; + request->output_path = argv[*index + 3]; + *index += 4; + return true; +} + +static bool cli_parse_filter_spec(int argc, + char **argv, + int *index, + cli_filter_spec_t *filter) { + if (*index + 5 >= argc) { + return false; + } + + if (strcmp(argv[*index], "-f") != 0 || strcmp(argv[*index + 2], "-h") != 0 || + strcmp(argv[*index + 4], "-w") != 0) { + return false; + } + + if (!cli_parse_filter(argv[*index + 1], &filter->kind) || + !cli_parse_size(argv[*index + 3], &filter->height) || + !cli_parse_size(argv[*index + 5], &filter->width)) { + return false; + } + + *index += 6; + + if (*index + 1 < argc && strcmp(argv[*index], "-t") == 0) { + if (!cli_parse_type(argv[*index + 1], &filter->direction)) { + return false; + } + *index += 2; + } + + return true; +} + +cli_parse_status_t cli_parse_args(int argc, + char **argv, + cli_request_t *request, + char *error_message, + size_t error_message_size) { + int index = 1; + + if (request == NULL || argv == NULL) { + return cli_invalid(error_message, error_message_size); + } + + cli_init_request(request); + + if (error_message != NULL && error_message_size != 0U) { + error_message[0] = '\0'; + } + + if (argc == 2 && strcmp(argv[1], "--help") == 0) { + cli_print_help(stdout, argv[0]); + return CLI_PARSE_HELP; + } + + if (argc < MIN_ARG_COUNT) { + return cli_invalid(error_message, error_message_size); + } + + if (!cli_parse_io_paths(argc, argv, &index, request)) { + return cli_invalid(error_message, error_message_size); + } + + if (!cli_parse_filter_spec(argc, argv, &index, &request->filters[0])) { + return cli_invalid(error_message, error_message_size); + } + + request->filter_count = 1U; + + if (index < argc && strcmp(argv[index], "-f") == 0) { + if (!cli_parse_filter_spec(argc, argv, &index, &request->filters[1])) { + return cli_invalid(error_message, error_message_size); + } + request->filter_count = CLI_MAX_FILTERS; + } + + if (strcmp(argv[index], "-s") != 0 || index + 1 != argc) { + return cli_invalid(error_message, error_message_size); + } + + return CLI_PARSE_OK; +} + +void cli_print_help(FILE *stream, const char *program_name) { + const char *name = program_name != NULL ? program_name : "main"; + + fprintf(stream, + "Usage:\n" + " %s -i -o -f -h -w " + "[-t ] -s\n" + " %s -i -o -f -h -w " + "[-t ] " + "-f -h -w [-t ] -s\n", + name, + name); +} diff --git a/src/cli_args.h b/src/cli_args.h new file mode 100644 index 0000000..acc0802 --- /dev/null +++ b/src/cli_args.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include + +#include + +#define CLI_MAX_FILTERS 2U + +typedef enum cli_parse_status { + CLI_PARSE_OK = 0, + CLI_PARSE_HELP, + CLI_PARSE_ERROR, +} cli_parse_status_t; + +typedef struct cli_filter_spec { + filter_kind_t kind; + size_t width; + size_t height; + filter_direction_t direction; + filter_border_mode_t border_mode; +} cli_filter_spec_t; + +typedef struct cli_request { + const char *input_path; + const char *output_path; + cli_filter_spec_t filters[CLI_MAX_FILTERS]; + size_t filter_count; +} cli_request_t; + +cli_parse_status_t cli_parse_args(int argc, + char **argv, + cli_request_t *request, + char *error_message, + size_t error_message_size); + +void cli_print_help(FILE *stream, const char *program_name); diff --git a/src/filters/blur_filter/blur_filter.c b/src/filters/blur_filter/blur_filter.c new file mode 100644 index 0000000..2dd8817 --- /dev/null +++ b/src/filters/blur_filter/blur_filter.c @@ -0,0 +1,60 @@ +#include "blur_filter.h" + +// clang-format off +static const double blur_kernel_3x3[] = { + 0.0, 0.2, 0.0, + 0.2, 0.2, 0.2, + 0.0, 0.2, 0.0 +}; + +static const double blur_factor_3x3 = 1.0; +static const double blur_bias_3x3 = 0.0; + +static const double blur_kernel_5x5[] = { + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 1.0, 1.0, 0.0, + 1.0, 1.0, 1.0, 1.0, 1.0, + 0.0, 1.0, 1.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, +}; + +static const double blur_factor_5x5 = 1.0 / 13.0; +static const double blur_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_blur_filter(filter_t *filter, size_t width, size_t height) { + + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + blur_kernel_3x3, + blur_factor_3x3, + blur_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + blur_kernel_5x5, + blur_factor_5x5, + blur_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/blur_filter/blur_filter.h b/src/filters/blur_filter/blur_filter.h new file mode 100644 index 0000000..9ed1bd5 --- /dev/null +++ b/src/filters/blur_filter/blur_filter.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +filter_status_t init_blur_filter(filter_t *filter, size_t width, size_t height); diff --git a/src/filters/emboss_filter/emboss_filter.c b/src/filters/emboss_filter/emboss_filter.c new file mode 100644 index 0000000..dabc9a8 --- /dev/null +++ b/src/filters/emboss_filter/emboss_filter.c @@ -0,0 +1,71 @@ +#include "emboss_filter.h" + +// clang-format off +static const double emboss_kernel_3x3[] = { + -1, -1, 0, + -1, 0, 1, + 0, 1, 1, +}; + +static const double emboss_factor_3x3 = 1.0; +static const double emboss_bias_3x3 = 128.0; + +static const double emboss_kernel_5x5[] = { + -1, -1, -1, -1, 0, + -1, -1, -1, 0, 1, + -1, -1, 0, 1, 1, + -1, 0, 1, 1, 1, + 0, 1, 1, 1, 1, +}; + +static const double emboss_factor_5x5 = 1.0; +static const double emboss_bias_5x5 = 128.0; +// clang-format on + +filter_status_t init_emboss_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (direction == FILTER_DIRECTION_NONE) { + direction = FILTER_DIRECTION_DIAGONAL_45; + } + + if (direction != FILTER_DIRECTION_DIAGONAL_45) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_EMBOSS, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + emboss_kernel_3x3, + emboss_factor_3x3, + emboss_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_EMBOSS, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + emboss_kernel_5x5, + emboss_factor_5x5, + emboss_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/emboss_filter/emboss_filter.h b/src/filters/emboss_filter/emboss_filter.h new file mode 100644 index 0000000..25e59f0 --- /dev/null +++ b/src/filters/emboss_filter/emboss_filter.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +filter_status_t init_emboss_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction); diff --git a/src/filters/filter.c b/src/filters/filter.c new file mode 100644 index 0000000..9d73a64 --- /dev/null +++ b/src/filters/filter.c @@ -0,0 +1,75 @@ +#include "filter.h" + +#include "blur_filter/blur_filter.h" +#include "emboss_filter/emboss_filter.h" +#include "find_edges_filter/find_edges_filter.h" +#include "gauss_blur_filter/gauss_blur_filter.h" +#include "mean_filter/mean_filter.h" +#include "median_filter/median_filter.h" +#include "motion_blur_filter/motion_blur_filter.h" +#include "sharpen_filter/sharpen_filter.h" + +filter_status_t filter_init_builtin(filter_t *filter, + const filter_request_t *request) { + filter_status_t status = FILTER_STATUS_UNSUPPORTED_KIND; + + if (!filter || !request) { + return FILTER_STATUS_NULL_POINTER; + } + + switch (request->kind) { + case FILTER_KIND_BLUR: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_MEAN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_mean_filter(filter, request->width, request->height); + break; + case FILTER_KIND_GAUSSIAN_BLUR: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_gauss_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_MOTION_BLUR: + if (request->direction != FILTER_DIRECTION_NONE && + request->direction != FILTER_DIRECTION_DIAGONAL_45) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_motion_blur_filter(filter, request->width, request->height); + break; + case FILTER_KIND_EDGE_DETECT: + status = init_find_edges_filter( + filter, request->width, request->height, request->direction); + break; + case FILTER_KIND_SHARPEN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_sharpen_filter(filter, request->width, request->height); + break; + case FILTER_KIND_EMBOSS: + status = init_emboss_filter( + filter, request->width, request->height, request->direction); + break; + case FILTER_KIND_MEDIAN: + if (request->direction != FILTER_DIRECTION_NONE) { + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } + status = init_median_filter(filter, request->width, request->height); + break; + default: + return FILTER_STATUS_UNSUPPORTED_KIND; + } + + if (status == FILTER_STATUS_OK) { + filter->border_mode = request->border_mode; + } + + return status; +} diff --git a/src/filters/filter.h b/src/filters/filter.h new file mode 100644 index 0000000..d3b6350 --- /dev/null +++ b/src/filters/filter.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include + +typedef enum filter_status { + FILTER_STATUS_OK = 0, + FILTER_STATUS_NULL_POINTER, + FILTER_STATUS_INVALID_ARGUMENT, + FILTER_STATUS_UNSUPPORTED_SIZE, + FILTER_STATUS_UNSUPPORTED_DIRECTION, + FILTER_STATUS_UNSUPPORTED_KIND +} filter_status_t; + +typedef enum filter_category { + FILTER_CATEGORY_SMOOTHING = 0, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_CATEGORY_EDGE_ENHANCEMENT, +} filter_category_t; + +typedef enum filter_operator { + FILTER_OPERATOR_CONVOLUTION = 0, + FILTER_OPERATOR_RANK_SELECTION, +} filter_operator_t; + +typedef enum filter_kind { + FILTER_KIND_BLUR = 0, + FILTER_KIND_MEAN, + FILTER_KIND_GAUSSIAN_BLUR, + FILTER_KIND_MOTION_BLUR, + FILTER_KIND_EDGE_DETECT, + FILTER_KIND_SHARPEN, + FILTER_KIND_EMBOSS, + FILTER_KIND_MEDIAN, +} filter_kind_t; + +typedef enum filter_direction { + FILTER_DIRECTION_NONE = 0, + FILTER_DIRECTION_HORIZONTAL, + FILTER_DIRECTION_VERTICAL, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_DIRECTION_OMNIDIRECTIONAL, +} filter_direction_t; + +typedef enum filter_border_mode { + FILTER_BORDER_WRAP = 0, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, +} filter_border_mode_t; + +typedef struct filter_request { + filter_kind_t kind; + size_t width; + size_t height; + filter_direction_t direction; + filter_border_mode_t border_mode; +} filter_request_t; + +typedef struct filter { + filter_category_t category; + filter_operator_t filter_operator; + filter_kind_t kind; + filter_direction_t direction; + filter_border_mode_t border_mode; + const double *kernel; + double factor; + double bias; + size_t width; + size_t height; + size_t rank_index; +} filter_t; + +static inline bool filter_size_is_odd(size_t value) { + return value != 0U && (value % 2U) == 1U; +} + +static inline size_t filter_median_rank(size_t width, size_t height) { + return (width * height) / 2U; +} + +static inline filter_request_t +make_filter_request(filter_kind_t kind, size_t width, size_t height) { + return (filter_request_t){ + .kind = kind, + .width = width, + .height = height, + .direction = FILTER_DIRECTION_NONE, + .border_mode = FILTER_BORDER_WRAP, + }; +} + +static inline filter_t make_convolution_filter(filter_kind_t kind, + filter_category_t category, + filter_direction_t direction, + filter_border_mode_t border_mode, + const double *kernel, + double factor, + double bias, + size_t width, + size_t height) { + return (filter_t){ + .category = category, + .filter_operator = FILTER_OPERATOR_CONVOLUTION, + .kind = kind, + .direction = direction, + .border_mode = border_mode, + .kernel = kernel, + .factor = factor, + .bias = bias, + .width = width, + .height = height, + .rank_index = 0U, + }; +} + +static inline filter_t make_rank_filter(filter_kind_t kind, + filter_category_t category, + filter_direction_t direction, + filter_border_mode_t border_mode, + size_t width, + size_t height, + size_t rank_index) { + return (filter_t){ + .category = category, + .filter_operator = FILTER_OPERATOR_RANK_SELECTION, + .kind = kind, + .direction = direction, + .border_mode = border_mode, + .kernel = NULL, + .factor = 0.0, + .bias = 0.0, + .width = width, + .height = height, + .rank_index = rank_index, + }; +} + +static inline bool filter_is_convolution(const filter_t *filter) { + return filter != NULL && + filter->filter_operator == FILTER_OPERATOR_CONVOLUTION; +} + +static inline bool filter_is_rank_selection(const filter_t *filter) { + return filter != NULL && + filter->filter_operator == FILTER_OPERATOR_RANK_SELECTION; +} + +static inline bool filter_has_explicit_kernel(const filter_t *filter) { + return filter_is_convolution(filter) && filter->kernel != NULL; +} + +filter_status_t filter_init_builtin(filter_t *filter, + const filter_request_t *request); diff --git a/src/filters/find_edges_filter/find_edges_filter.c b/src/filters/find_edges_filter/find_edges_filter.c new file mode 100644 index 0000000..7e79b31 --- /dev/null +++ b/src/filters/find_edges_filter/find_edges_filter.c @@ -0,0 +1,121 @@ +#include "find_edges_filter.h" + +// clang-format off +static const double find_edge_kernel_horizontal[] = { + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, 2, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, +}; + +static const double find_edge_horizontal_factor = 1.0; +static const double find_edge_horizontal_bias = 0.0; + +static const double find_edge_kernel_vertical[] = { + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, 4, 0, 0, + 0, 0, -1, 0, 0, + 0, 0, -1, 0, 0, +}; + +static const double find_edge_vertical_factor = 1.0; +static const double find_edge_vertical_bias = 0.0; + +static const double find_edge_kernel_diagonal45deg[] = { + -1, 0, 0, 0, 0, + 0, -2, 0, 0, 0, + 0, 0, 6, 0, 0, + 0, 0, 0, -2, 0, + 0, 0, 0, 0, -1, +}; + +static const double find_edge_diagonal45deg_factor = 1.0; +static const double find_edge_diagonal45deg_bias = 0.0; + + +static const double find_edge_kernel_any_direction[] = { + -1, -1, -1, + -1, 8, -1, + -1, -1, -1 +}; + +static const double find_edge_any_direction_factor = 1.0; +static const double find_edge_any_direction_bias = 0.0; + +// clang-format on + +filter_status_t init_find_edges_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (direction == FILTER_DIRECTION_NONE) { + direction = FILTER_DIRECTION_OMNIDIRECTIONAL; + } + + switch (direction) { + case FILTER_DIRECTION_HORIZONTAL: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_HORIZONTAL, + FILTER_BORDER_WRAP, + find_edge_kernel_horizontal, + find_edge_horizontal_factor, + find_edge_horizontal_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_VERTICAL: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_VERTICAL, + FILTER_BORDER_WRAP, + find_edge_kernel_vertical, + find_edge_vertical_factor, + find_edge_vertical_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_DIAGONAL_45: + if (width != 5 || height != 5) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + find_edge_kernel_diagonal45deg, + find_edge_diagonal45deg_factor, + find_edge_diagonal45deg_bias, + width, + height); + return FILTER_STATUS_OK; + case FILTER_DIRECTION_OMNIDIRECTIONAL: + if (width != 3 || height != 3) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + *filter = make_convolution_filter(FILTER_KIND_EDGE_DETECT, + FILTER_CATEGORY_EDGE_DETECTION, + FILTER_DIRECTION_OMNIDIRECTIONAL, + FILTER_BORDER_WRAP, + find_edge_kernel_any_direction, + find_edge_any_direction_factor, + find_edge_any_direction_bias, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_DIRECTION; + } +} diff --git a/src/filters/find_edges_filter/find_edges_filter.h b/src/filters/find_edges_filter/find_edges_filter.h new file mode 100644 index 0000000..b0866d8 --- /dev/null +++ b/src/filters/find_edges_filter/find_edges_filter.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +filter_status_t init_find_edges_filter(filter_t *filter, + size_t width, + size_t height, + filter_direction_t direction); diff --git a/src/filters/gauss_blur_filter/gauss_blur_filter.c b/src/filters/gauss_blur_filter/gauss_blur_filter.c new file mode 100644 index 0000000..2f16b79 --- /dev/null +++ b/src/filters/gauss_blur_filter/gauss_blur_filter.c @@ -0,0 +1,59 @@ +#include "gauss_blur_filter.h" + +// clang-format off +static const double gauss_blur_kernel_3x3[] = { + 1, 2, 1, + 2, 4, 2, + 1, 2, 1, +}; + +static const double blur_factor_3x3 = 1.0 / 16.0; +static const double blur_bias_3x3 = 0.0; + +static const double gauss_blur_kernel_5x5[] = { + 1, 4, 6, 4, 1, + 4, 16, 24, 16, 4, + 6, 24, 36, 24, 6, + 4, 16, 24, 16, 4, + 1, 4, 6, 4, 1, +}; + +static const double blur_factor_5x5 = 1.0 / 256.0; +static const double blur_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_gauss_blur_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_GAUSSIAN_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + gauss_blur_kernel_3x3, + blur_factor_3x3, + blur_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_GAUSSIAN_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + gauss_blur_kernel_5x5, + blur_factor_5x5, + blur_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/gauss_blur_filter/gauss_blur_filter.h b/src/filters/gauss_blur_filter/gauss_blur_filter.h new file mode 100644 index 0000000..9d61e35 --- /dev/null +++ b/src/filters/gauss_blur_filter/gauss_blur_filter.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +filter_status_t +init_gauss_blur_filter(filter_t *filter, size_t width, size_t height); diff --git a/src/filters/mean_filter/mean_filter.c b/src/filters/mean_filter/mean_filter.c new file mode 100644 index 0000000..0df228f --- /dev/null +++ b/src/filters/mean_filter/mean_filter.c @@ -0,0 +1,60 @@ +#include "mean_filter.h" + +// clang-format off +static const double mean_kernel_3x3[] = { + 1, 1, 1, + 1, 1, 1, + 1, 1, 1, +}; + +static const double mean_factor_3x3 = 1.0 / 9.0; +static const double mean_bias_3x3 = 0.0; + +static const double mean_kernel_5x5[] = { + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, +}; + +static const double mean_factor_5x5 = 1.0 / 25.0; +static const double mean_bias_5x5 = 0.0; +// clang-format on + +filter_status_t init_mean_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_MEAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + mean_kernel_3x3, + mean_factor_3x3, + mean_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_MEAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + mean_kernel_5x5, + mean_factor_5x5, + mean_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/mean_filter/mean_filter.h b/src/filters/mean_filter/mean_filter.h new file mode 100644 index 0000000..5492c79 --- /dev/null +++ b/src/filters/mean_filter/mean_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_mean_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/median_filter/median_filter.c b/src/filters/median_filter/median_filter.c new file mode 100644 index 0000000..b28ebac --- /dev/null +++ b/src/filters/median_filter/median_filter.c @@ -0,0 +1,22 @@ +#include "median_filter.h" + +filter_status_t +init_median_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (!filter_size_is_odd(width) || !filter_size_is_odd(height)) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + *filter = make_rank_filter(FILTER_KIND_MEDIAN, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + width, + height, + filter_median_rank(width, height)); + + return FILTER_STATUS_OK; +} diff --git a/src/filters/median_filter/median_filter.h b/src/filters/median_filter/median_filter.h new file mode 100644 index 0000000..11e313e --- /dev/null +++ b/src/filters/median_filter/median_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_median_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/motion_blur_filter/motion_blur_filter.c b/src/filters/motion_blur_filter/motion_blur_filter.c new file mode 100644 index 0000000..2f8017a --- /dev/null +++ b/src/filters/motion_blur_filter/motion_blur_filter.c @@ -0,0 +1,44 @@ +#include "motion_blur_filter.h" + +// clang-format off + +static const double blur_kernel_9x9[] = { + 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 1, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, +}; + +static const double blur_factor_9x9 = 1.0 / 9.0; +static const double blur_bias = 0.0; +// clang-format on + +filter_status_t +init_motion_blur_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + switch (width) { + case 9: + *filter = make_convolution_filter(FILTER_KIND_MOTION_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_DIAGONAL_45, + FILTER_BORDER_WRAP, + blur_kernel_9x9, + blur_factor_9x9, + blur_bias, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/motion_blur_filter/motion_blur_filter.h b/src/filters/motion_blur_filter/motion_blur_filter.h new file mode 100644 index 0000000..62a3426 --- /dev/null +++ b/src/filters/motion_blur_filter/motion_blur_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_motion_blur_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/filters/sharpen_filter/sharpen_filter.c b/src/filters/sharpen_filter/sharpen_filter.c new file mode 100644 index 0000000..8acf2c9 --- /dev/null +++ b/src/filters/sharpen_filter/sharpen_filter.c @@ -0,0 +1,61 @@ +#include "sharpen_filter.h" + +// clang-format off +static const double sharpen_kernel_3x3[] = { + -1, -1, -1, + -1, 9, -1, + -1, -1, -1, +}; + +static const double sharpen_factor_3x3 = 1.0; +static const double sharpen_bias_3x3 = 0.0; + +static const double sharpen_kernel_5x5[] = { + -1, -1, -1, -1, -1, + -1, 2, 2, 2, -1, + -1, 2, 8, 2, -1, + -1, 2, 2, 2, -1, + -1, -1, -1, -1, -1, +}; + +static const double sharpen_factor_5x5 = 1.0 / 8.0; +static const double sharpen_bias_5x5 = 0.0; +// clang-format on + +filter_status_t +init_sharpen_filter(filter_t *filter, size_t width, size_t height) { + if (!filter) { + return FILTER_STATUS_NULL_POINTER; + } + + if (width != height) { + return FILTER_STATUS_UNSUPPORTED_SIZE; + } + + switch (width) { + case 3: + *filter = make_convolution_filter(FILTER_KIND_SHARPEN, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + sharpen_kernel_3x3, + sharpen_factor_3x3, + sharpen_bias_3x3, + width, + height); + return FILTER_STATUS_OK; + case 5: + *filter = make_convolution_filter(FILTER_KIND_SHARPEN, + FILTER_CATEGORY_EDGE_ENHANCEMENT, + FILTER_DIRECTION_NONE, + FILTER_BORDER_WRAP, + sharpen_kernel_5x5, + sharpen_factor_5x5, + sharpen_bias_5x5, + width, + height); + return FILTER_STATUS_OK; + default: + return FILTER_STATUS_UNSUPPORTED_SIZE; + } +} diff --git a/src/filters/sharpen_filter/sharpen_filter.h b/src/filters/sharpen_filter/sharpen_filter.h new file mode 100644 index 0000000..61f40c2 --- /dev/null +++ b/src/filters/sharpen_filter/sharpen_filter.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +filter_status_t init_sharpen_filter(filter_t *filter, + size_t width, + size_t height); diff --git a/src/image_helpers/image_helpers.h b/src/image_helpers/image_helpers.h new file mode 100644 index 0000000..5b6e3aa --- /dev/null +++ b/src/image_helpers/image_helpers.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +typedef struct image_view { + unsigned char *data; + size_t height; + size_t width; + size_t stride; + size_t channels; +} image_view_t; diff --git a/src/sequentially_convolution/sequentially_convolution.c b/src/sequentially_convolution/sequentially_convolution.c new file mode 100644 index 0000000..4f0d5fd --- /dev/null +++ b/src/sequentially_convolution/sequentially_convolution.c @@ -0,0 +1,86 @@ +#include "sequentially_convolution.h" + +#include +#include +#include +#include + +int sequential_convolution(const filter_t *filter, image_view_t *image_view) { + if (filter == NULL || image_view == NULL || image_view->data == NULL) { + return -1; + } + if (!filter_is_convolution(filter) || !filter_has_explicit_kernel(filter)) { + return -1; + } + if (filter->width == 0U || filter->height == 0U) { + return -1; + } + if (image_view->width == 0U || image_view->height == 0U || + image_view->channels == 0U) { + return -1; + } + if (image_view->width > (SIZE_MAX / image_view->channels)) { + return -1; + } + if (image_view->stride < image_view->width * image_view->channels) { + return -1; + } + if (image_view->height > (SIZE_MAX / image_view->stride)) { + return -1; + } + + const size_t height = image_view->height; + const size_t width = image_view->width; + const size_t stride = image_view->stride; + const size_t channels = image_view->channels; + unsigned char *pixels = image_view->data; + const size_t buffer_size = height * stride; + + unsigned char *source_copy = (unsigned char *)malloc(buffer_size); + if (source_copy == NULL) { + return -1; + } + + memcpy(source_copy, pixels, buffer_size); + + for (size_t y = 0; y < height; ++y) { + for (size_t x = 0; x < width; ++x) { + unsigned char *dst_pixel = pixels + y * stride + x * channels; + + for (size_t channel = 0; channel < channels; ++channel) { + if (channels == 4U && channel == 3U) { + dst_pixel[channel] = source_copy[y * stride + x * channels + channel]; + continue; + } + + double sum = 0.0; + + for (size_t filter_y = 0; filter_y < filter->height; ++filter_y) { + for (size_t filter_x = 0; filter_x < filter->width; ++filter_x) { + const long src_x = + (long)x - (long)(filter->width / 2U) + (long)filter_x; + const long src_y = + (long)y - (long)(filter->height / 2U) + (long)filter_y; + + const size_t image_x = + resolve_index(src_x, width, filter->border_mode); + const size_t image_y = + resolve_index(src_y, height, filter->border_mode); + + const unsigned char *src_pixel = + source_copy + image_y * stride + image_x * channels; + const double kernel_value = + filter->kernel[filter_y * filter->width + filter_x]; + + sum += (double)src_pixel[channel] * kernel_value; + } + } + + dst_pixel[channel] = clamp_to_u8(filter->factor * sum + filter->bias); + } + } + } + + free(source_copy); + return 0; +} diff --git a/src/sequentially_convolution/sequentially_convolution.h b/src/sequentially_convolution/sequentially_convolution.h new file mode 100644 index 0000000..d4731bb --- /dev/null +++ b/src/sequentially_convolution/sequentially_convolution.h @@ -0,0 +1,7 @@ +#pragma once + +#include +#include +#include + +int sequential_convolution(const filter_t *filter, image_view_t *image_view); diff --git a/test/sequential_tests/CMakeLists.txt b/test/sequential_tests/CMakeLists.txt new file mode 100644 index 0000000..d5d04b7 --- /dev/null +++ b/test/sequential_tests/CMakeLists.txt @@ -0,0 +1,23 @@ +add_executable(sequential_tests + sequential_tests.c +) + +target_link_libraries(sequential_tests + PRIVATE + convolution_runtime + PkgConfig::CMOCKA +) + +add_test(NAME sequential_tests COMMAND sequential_tests) + +add_executable(sequential_opencv_reference_tests + sequential_opencv_reference_tests.c +) + +target_link_libraries(sequential_opencv_reference_tests + PRIVATE + convolution_runtime + PkgConfig::CMOCKA +) + +add_test(NAME sequential_opencv_reference_tests COMMAND sequential_opencv_reference_tests) diff --git a/test/sequential_tests/sequential_opencv_reference_tests.c b/test/sequential_tests/sequential_opencv_reference_tests.c new file mode 100644 index 0000000..be2b8d5 --- /dev/null +++ b/test/sequential_tests/sequential_opencv_reference_tests.c @@ -0,0 +1,419 @@ +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_IMAGE_BYTES 2048U +#define TEST_NAME_SIZE 128U +#define RANDOM_PIXEL_MASK 63U +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +typedef struct test_image { + unsigned char data[MAX_IMAGE_BYTES]; + image_view_t view; + size_t size; +} test_image_t; + +typedef struct kernel_sample { + const char *name; + const double *values; + size_t width; + size_t height; + double factor; + double bias; +} kernel_sample_t; + +static uint32_t random_state = 0x12345678U; + +static unsigned char random_byte(void) { + random_state = random_state * 1664525U + 1013904223U; + return (unsigned char)(random_state >> 24U); +} + +static void init_image(test_image_t *image, + size_t width, + size_t height, + size_t channels, + size_t padding) { + const size_t stride = width * channels + padding; + + image->size = height * stride; + image->view = (image_view_t){ + .data = image->data, + .height = height, + .width = width, + .stride = stride, + .channels = channels, + }; + + memset(image->data, 0, sizeof(image->data)); +} + +static unsigned char *pixel(test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static const unsigned char * +const_pixel(const test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static void fill_random(test_image_t *image) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + unsigned char *current_pixel = pixel(image, x, y); + + for (size_t channel = 0; channel < image->view.channels; ++channel) { + current_pixel[channel] = random_byte() & RANDOM_PIXEL_MASK; + } + } + + for (size_t i = image->view.width * image->view.channels; + i < image->view.stride; + ++i) { + image->data[y * image->view.stride + i] = random_byte(); + } + } +} + +static void copy_image(test_image_t *to, const test_image_t *from) { + memcpy(to->data, from->data, from->size); + to->size = from->size; + to->view = from->view; + to->view.data = to->data; +} + +static unsigned char clamp_to_byte(double value) { + if (value < 0.0) { + return 0U; + } + if (value > 255.0) { + return 255U; + } + return (unsigned char)(value + 0.5); +} + +static int cv_border(filter_border_mode_t border) { + switch (border) { + case FILTER_BORDER_WRAP: + return IPL_BORDER_WRAP; + case FILTER_BORDER_REFLECT: + return IPL_BORDER_REFLECT_101; + case FILTER_BORDER_CLAMP: + default: + return IPL_BORDER_REPLICATE; + } +} + +static const char *border_name(filter_border_mode_t border) { + switch (border) { + case FILTER_BORDER_WRAP: + return "wrap"; + case FILTER_BORDER_REFLECT: + return "reflect"; + case FILTER_BORDER_CLAMP: + default: + return "clamp"; + } +} + +static filter_t make_test_filter(const kernel_sample_t *kernel, + filter_border_mode_t border) { + return make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + border, + kernel->values, + kernel->factor, + kernel->bias, + kernel->width, + kernel->height); +} + +static CvMat *make_cv_kernel(const kernel_sample_t *kernel) { + CvMat *cv_kernel = + cvCreateMat((int)kernel->height, (int)kernel->width, CV_64FC1); + + if (cv_kernel == NULL) { + return NULL; + } + + for (size_t y = 0; y < kernel->height; ++y) { + for (size_t x = 0; x < kernel->width; ++x) { + const double value = kernel->values[y * kernel->width + x]; + cvmSet(cv_kernel, (int)y, (int)x, value * kernel->factor); + } + } + + return cv_kernel; +} + +static CvMat *run_opencv_filter(const test_image_t *source, + const kernel_sample_t *kernel, + filter_border_mode_t border) { + const int channels = (int)source->view.channels; + const int input_type = CV_MAKETYPE(CV_8U, channels); + const int result_type = CV_MAKETYPE(CV_64F, channels); + const int radius_x = (int)(kernel->width / 2U); + const int radius_y = (int)(kernel->height / 2U); + + CvMat source_header; + CvMat *padded = NULL; + CvMat *result = NULL; + CvMat *cv_kernel = NULL; + + cvInitMatHeader(&source_header, + (int)source->view.height, + (int)source->view.width, + input_type, + (void *)source->data, + (int)source->view.stride); + + padded = cvCreateMat((int)source->view.height + 2 * radius_y, + (int)source->view.width + 2 * radius_x, + input_type); + result = cvCreateMat((int)source->view.height + 2 * radius_y, + (int)source->view.width + 2 * radius_x, + result_type); + cv_kernel = make_cv_kernel(kernel); + + if (padded == NULL || result == NULL || cv_kernel == NULL) { + cvReleaseMat(&padded); + cvReleaseMat(&result); + cvReleaseMat(&cv_kernel); + return NULL; + } + + /* cvFilter2D from OpenCV 2 C API always uses its own border mode. + To compare the same border handling as our code, pad the image first. */ + cvCopyMakeBorder(&source_header, + padded, + cvPoint(radius_x, radius_y), + cv_border(border), + cvScalarAll(0.0)); + cvFilter2D(padded, result, cv_kernel, cvPoint(radius_x, radius_y)); + + cvReleaseMat(&padded); + cvReleaseMat(&cv_kernel); + return result; +} + +static int padding_is_same(const test_image_t *before, + const test_image_t *after) { + const size_t row_pixels = after->view.width * after->view.channels; + + for (size_t y = 0; y < after->view.height; ++y) { + const unsigned char *before_padding = + before->data + y * before->view.stride + row_pixels; + const unsigned char *after_padding = + after->data + y * after->view.stride + row_pixels; + const size_t padding_size = after->view.stride - row_pixels; + + if (memcmp(before_padding, after_padding, padding_size) != 0) { + return 0; + } + } + + return 1; +} + +static int compare_with_opencv(const test_image_t *source, + const test_image_t *actual, + const kernel_sample_t *kernel, + const CvMat *opencv_result, + const char *test_name) { + const int radius_x = (int)(kernel->width / 2U); + const int radius_y = (int)(kernel->height / 2U); + + for (size_t y = 0; y < actual->view.height; ++y) { + const double *opencv_row = + (const double *)(const void *)(opencv_result->data.ptr + + ((int)y + radius_y) * opencv_result->step); + + for (size_t x = 0; x < actual->view.width; ++x) { + const unsigned char *source_pixel = const_pixel(source, x, y); + const unsigned char *actual_pixel = const_pixel(actual, x, y); + + for (size_t channel = 0; channel < actual->view.channels; ++channel) { + const double cv_value = + opencv_row[((int)x + radius_x) * actual->view.channels + channel] + + kernel->bias; + const unsigned char expected = + (actual->view.channels == 4U && channel == 3U) + ? source_pixel[channel] + : clamp_to_byte(cv_value); + + if (actual_pixel[channel] != expected) { + fail_msg("%s: pixel (%zu, %zu), channel %zu: got %u, expected %u", + test_name, + x, + y, + channel, + (unsigned)actual_pixel[channel], + (unsigned)expected); + return 0; + } + } + } + } + + if (!padding_is_same(source, actual)) { + fail_msg("%s: image row padding changed", test_name); + return 0; + } + + return 1; +} + +static void check_case(size_t width, + size_t height, + size_t channels, + const kernel_sample_t *kernel, + filter_border_mode_t border) { + const size_t padding = (width + height + channels) % 4U; + char test_name[TEST_NAME_SIZE]; + test_image_t source; + test_image_t actual; + CvMat *opencv_result = NULL; + + snprintf(test_name, + sizeof(test_name), + "%zux%zux%zu, %s, %s border", + width, + height, + channels, + kernel->name, + border_name(border)); + + init_image(&source, width, height, channels, padding); + fill_random(&source); + copy_image(&actual, &source); + + filter_t filter = make_test_filter(kernel, border); + assert_int_equal(sequential_convolution(&filter, &actual.view), 0); + + opencv_result = run_opencv_filter(&source, kernel, border); + assert_non_null(opencv_result); + + assert_true( + compare_with_opencv(&source, &actual, kernel, opencv_result, test_name)); + cvReleaseMat(&opencv_result); +} + +static void sequential_convolution_matches_opencv(void **state) { + (void)state; + + static const double identity[] = {1.0}; + static const double gaussian_3x3[] = { + 1.0, + 2.0, + 1.0, + 2.0, + 4.0, + 2.0, + 1.0, + 2.0, + 1.0, + }; + static const double sharpen_3x3[] = { + 0.0, + -1.0, + 0.0, + -1.0, + 5.0, + -1.0, + 0.0, + -1.0, + 0.0, + }; + static const double tall_3x5[] = { + 0.0, + 1.0, + 0.0, + -1.0, + 2.0, + -1.0, + 0.0, + 3.0, + 0.0, + -1.0, + 2.0, + -1.0, + 0.0, + 1.0, + 0.0, + }; + static const double wide_5x3[] = { + 0.0, + -1.0, + 0.0, + 1.0, + 0.0, + 1.0, + 2.0, + 3.0, + 2.0, + 1.0, + 0.0, + 1.0, + 0.0, + -1.0, + 0.0, + }; + + const size_t widths[] = {1U, 2U, 3U, 5U, 8U, 13U}; + const size_t heights[] = {1U, 2U, 4U, 7U}; + const size_t channels[] = {1U, 3U, 4U}; + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + const kernel_sample_t kernels[] = { + {"identity", identity, 1U, 1U, 1.0, 0.0}, + {"gaussian 3x3", gaussian_3x3, 3U, 3U, 1.0 / 16.0, 0.0}, + {"sharpen 3x3", sharpen_3x3, 3U, 3U, 1.0, 0.0}, + {"3x5", tall_3x5, 3U, 5U, 1.0, 3.0}, + {"5x3", wide_5x3, 5U, 3U, 1.0, 7.0}, + }; + + for (size_t width_index = 0; width_index < ARRAY_SIZE(widths); + ++width_index) { + for (size_t height_index = 0; height_index < ARRAY_SIZE(heights); + ++height_index) { + for (size_t channel_index = 0; channel_index < ARRAY_SIZE(channels); + ++channel_index) { + for (size_t kernel_index = 0; kernel_index < ARRAY_SIZE(kernels); + ++kernel_index) { + for (size_t border_index = 0; border_index < ARRAY_SIZE(borders); + ++border_index) { + check_case(widths[width_index], + heights[height_index], + channels[channel_index], + &kernels[kernel_index], + borders[border_index]); + } + } + } + } + } +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(sequential_convolution_matches_opencv), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +} diff --git a/test/sequential_tests/sequential_tests.c b/test/sequential_tests/sequential_tests.c new file mode 100644 index 0000000..916dd72 --- /dev/null +++ b/test/sequential_tests/sequential_tests.c @@ -0,0 +1,363 @@ +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#define MAX_IMAGE_BYTES 4096U +#define ARRAY_SIZE(array) (sizeof(array) / sizeof((array)[0])) + +typedef struct test_image { + unsigned char data[MAX_IMAGE_BYTES]; + image_view_t view; + size_t size; +} test_image_t; + +static uint32_t random_state = 0x12345678U; + +static unsigned char random_byte(void) { + random_state = random_state * 1664525U + 1013904223U; + return (unsigned char)(random_state >> 24U); +} + +static void init_image(test_image_t *image, + size_t width, + size_t height, + size_t channels, + size_t padding) { + const size_t stride = width * channels + padding; + + image->size = height * stride; + image->view = (image_view_t){ + .data = image->data, + .height = height, + .width = width, + .stride = stride, + .channels = channels, + }; + + memset(image->data, 0xCD, sizeof(image->data)); +} + +static void fill_random(test_image_t *image) { + for (size_t i = 0; i < image->size; ++i) { + image->data[i] = random_byte(); + } +} + +static void copy_image(test_image_t *to, const test_image_t *from) { + memcpy(to->data, from->data, from->size); +} + +static int same_image(const test_image_t *left, const test_image_t *right) { + if (left->size != right->size) { + return 0; + } + return memcmp(left->data, right->data, left->size) == 0; +} + +static filter_t make_filter(const double *kernel, + size_t width, + size_t height, + double factor, + filter_border_mode_t border_mode) { + return make_convolution_filter(FILTER_KIND_BLUR, + FILTER_CATEGORY_SMOOTHING, + FILTER_DIRECTION_NONE, + border_mode, + kernel, + factor, + 0.0, + width, + height); +} + +static void apply_filter(test_image_t *image, + const double *kernel, + size_t width, + size_t height, + double factor, + filter_border_mode_t border_mode) { + filter_t filter = make_filter(kernel, width, height, factor, border_mode); + assert_int_equal(sequential_convolution(&filter, &image->view), 0); +} + +static void make_identity_kernel(double *kernel, size_t size) { + memset(kernel, 0, size * size * sizeof(*kernel)); + kernel[(size / 2U) * size + size / 2U] = 1.0; +} + +static void make_shift_kernel(double *kernel, int dx, int dy) { + const int center = 1; + + memset(kernel, 0, 9U * sizeof(*kernel)); + kernel[(size_t)(center + dy) * 3U + (size_t)(center + dx)] = 1.0; +} + +static unsigned char *pixel(test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static const unsigned char * +const_pixel(const test_image_t *image, size_t x, size_t y) { + return image->data + y * image->view.stride + x * image->view.channels; +} + +static void set_one_channel_image(test_image_t *image, + const unsigned char *values) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + *pixel(image, x, y) = values[y * image->view.width + x]; + } + } +} + +static int one_channel_image_has_values(const test_image_t *image, + const unsigned char *values) { + for (size_t y = 0; y < image->view.height; ++y) { + for (size_t x = 0; x < image->view.width; ++x) { + if (*const_pixel(image, x, y) != values[y * image->view.width + x]) { + return 0; + } + } + } + return 1; +} + +static void identity_filter_keeps_image(void **state) { + (void)state; + const size_t kernel_sizes[] = {1U, 3U, 5U, 7U}; + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + + for (size_t channels = 1U; channels <= 4U; ++channels) { + if (channels == 2U) { + continue; + } + + for (size_t i = 0; i < ARRAY_SIZE(kernel_sizes); + ++i) { + double kernel[49]; + test_image_t original; + test_image_t actual; + + init_image(&original, 8U, 5U, channels, 3U); + init_image(&actual, 8U, 5U, channels, 3U); + + fill_random(&original); + make_identity_kernel(kernel, kernel_sizes[i]); + + for (size_t b = 0; b < ARRAY_SIZE(borders); ++b) { + copy_image(&actual, &original); + apply_filter( + &actual, kernel, kernel_sizes[i], kernel_sizes[i], 1.0, borders[b]); + assert_true(same_image(&original, &actual)); + } + } + } + +} + +static void zero_filter_makes_rgb_black_and_keeps_alpha(void **state) { + (void)state; + double zero_kernel[9] = {0.0}; + test_image_t original; + test_image_t actual; + + init_image(&original, 7U, 4U, 4U, 2U); + init_image(&actual, 7U, 4U, 4U, 2U); + + fill_random(&original); + copy_image(&actual, &original); + + apply_filter(&actual, zero_kernel, 3U, 3U, 1.0, FILTER_BORDER_REFLECT); + + for (size_t y = 0; y < actual.view.height; ++y) { + for (size_t x = 0; x < actual.view.width; ++x) { + const unsigned char *before = const_pixel(&original, x, y); + const unsigned char *after = const_pixel(&actual, x, y); + + assert_int_equal(after[0], 0U); + assert_int_equal(after[1], 0U); + assert_int_equal(after[2], 0U); + assert_int_equal(after[3], before[3]); + } + } + +} + +static void shift_filter_respects_border_modes(void **state) { + (void)state; + double shift_right[9]; + + // clang-format off + const unsigned char source_values[] = { + 1U, 2U, 3U, + 4U, 5U, 6U, + }; + const unsigned char wrap_expected[] = { + 2U, 3U, 1U, + 5U, 6U, 4U, + }; + const unsigned char clamp_expected[] = { + 2U, 3U, 3U, + 5U, 6U, 6U, + }; + const unsigned char reflect_expected[] = { + 2U, 3U, 2U, + 5U, 6U, 5U, + }; + // clang-format on + + test_image_t image; + + init_image(&image, 3U, 2U, 1U, 1U); + + make_shift_kernel(shift_right, 1, 0); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(one_channel_image_has_values(&image, wrap_expected)); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_CLAMP); + assert_true(one_channel_image_has_values(&image, clamp_expected)); + + set_one_channel_image(&image, source_values); + apply_filter(&image, shift_right, 3U, 3U, 1.0, FILTER_BORDER_REFLECT); + assert_true(one_channel_image_has_values(&image, reflect_expected)); + +} + +static void opposite_shifts_compose_to_identity_with_wrap(void **state) { + (void)state; + double shift_right[9]; + double shift_left[9]; + double shift_down[9]; + double shift_up[9]; + test_image_t original; + test_image_t actual; + + init_image(&original, 9U, 7U, 3U, 4U); + init_image(&actual, 9U, 7U, 3U, 4U); + + make_shift_kernel(shift_right, 1, 0); + make_shift_kernel(shift_left, -1, 0); + make_shift_kernel(shift_down, 0, 1); + make_shift_kernel(shift_up, 0, -1); + + fill_random(&original); + + copy_image(&actual, &original); + apply_filter(&actual, shift_right, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + apply_filter(&actual, shift_left, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(same_image(&original, &actual)); + + copy_image(&actual, &original); + apply_filter(&actual, shift_down, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + apply_filter(&actual, shift_up, 3U, 3U, 1.0, FILTER_BORDER_WRAP); + assert_true(same_image(&original, &actual)); + +} + +static void zero_padded_kernel_gives_same_result(void **state) { + (void)state; + + // clang-format off + const double kernel_3x3[9] = { + 0.0, 1.0, 0.0, + 1.0, 4.0, 1.0, + 0.0, 1.0, 0.0, + }; + const double same_kernel_padded_to_5x5[25] = { + 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 4.0, 1.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, + }; + // clang-format on + + const filter_border_mode_t borders[] = { + FILTER_BORDER_WRAP, + FILTER_BORDER_CLAMP, + FILTER_BORDER_REFLECT, + }; + test_image_t source; + test_image_t small_kernel_result; + test_image_t padded_kernel_result; + + init_image(&source, 10U, 6U, 3U, 2U); + init_image(&small_kernel_result, 10U, 6U, 3U, 2U); + init_image(&padded_kernel_result, 10U, 6U, 3U, 2U); + + fill_random(&source); + + for (size_t i = 0; i < ARRAY_SIZE(borders); ++i) { + copy_image(&small_kernel_result, &source); + copy_image(&padded_kernel_result, &source); + + apply_filter(&small_kernel_result, kernel_3x3, 3U, 3U, 1.0, borders[i]); + apply_filter(&padded_kernel_result, + same_kernel_padded_to_5x5, + 5U, + 5U, + 1.0, + borders[i]); + assert_true(same_image(&small_kernel_result, &padded_kernel_result)); + } + +} + +static void known_wrap_mean_3x3(void **state) { + (void)state; + double mean_kernel[9]; + test_image_t image; + + init_image(&image, 3U, 3U, 1U, 0U); + + // clang-format off + const unsigned char values[] = { + 1U, 2U, 3U, + 4U, 5U, 6U, + 7U, 8U, 9U, + }; + const unsigned char expected[] = { + 5U, 5U, 5U, + 5U, 5U, 5U, + 5U, 5U, 5U, + }; + // clang-format on + + for (size_t i = 0; i < 9U; ++i) { + mean_kernel[i] = 1.0; + } + + set_one_channel_image(&image, values); + apply_filter(&image, mean_kernel, 3U, 3U, 1.0 / 9.0, FILTER_BORDER_WRAP); + assert_true(one_channel_image_has_values(&image, expected)); + +} + +int main(void) { + const struct CMUnitTest tests[] = { + cmocka_unit_test(identity_filter_keeps_image), + cmocka_unit_test(zero_filter_makes_rgb_black_and_keeps_alpha), + cmocka_unit_test(shift_filter_respects_border_modes), + cmocka_unit_test(opposite_shifts_compose_to_identity_with_wrap), + cmocka_unit_test(zero_padded_kernel_gives_same_result), + cmocka_unit_test(known_wrap_mean_3x3), + }; + + return cmocka_run_group_tests(tests, NULL, NULL); +}