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..a321ee9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.cache +/build +/benchmarks/processed_outputs +/benchmarks/pipeline_outputs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..a787dea --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,137 @@ +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) +find_package(Threads REQUIRED) +find_package(OpenMP REQUIRED) + +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/convolution/*.c" + "${CMAKE_CURRENT_SOURCE_DIR}/src/parallel_convolution/*.c" + "${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 + Threads::Threads + OpenMP::OpenMP_C +) + +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..4cd76f1 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # Image Conversation -Основная ветка намеренно оставлена минимальной. - Реализации задач разнесены по отдельным веткам: - `feat/sequential_convolution` — последовательная свёртка. diff --git a/docs/task2/README.md b/docs/task2/README.md new file mode 100644 index 0000000..004030b --- /dev/null +++ b/docs/task2/README.md @@ -0,0 +1,83 @@ +# Бенчмарк параллельной обработки изображений + +Измерялась зависимость времени исполнения от количества потоков для параллельной реализации свёртки. На каждом графике показаны четыре способа параллелизма (по столбцам, по строкам, по пикселям, по гриду): `cols`, `raw`, `pixel`, `grid`. + +Во всех случаях применялась одна и та же композиция фильтров: `motion_blur 9x9 + gauss 5x5`. По вертикали отложено время исполнения в миллисекундах, по горизонтали - количество потоков. Цветные прямоугольники рядом с каждым значением потоков показывают режим разбиения и среднее время по 10 запускам. Интервалы погрешности построены по 95-му перцентилю: в таблицах значение записано как `mean +- (p95 - mean)`. + +Измерялось только время применения композиции фильтров к уже загруженному изображению. Чтение изображения, подготовка копии входного буфера, запись результата и построение графиков в измерение не входят. + +## Аппаратные ресурсы + +| Параметр | Значение | +|---|---:| +| Архитектура | `x86_64` | +| CPU | `Intel(R) Core(TM) Ultra 5 125H` | +| Логические CPU | `18` | +| Ядер на сокет | `14` | +| Потоков на ядро | `2` | +| Сокетов | `1` | +| CPU scaling | `62%` | +| CPU max/min MHz | `4600 / 400` | +| L1d cache | `448 KiB (12 instances)` | +| L1i cache | `768 KiB (12 instances)` | +| L2 cache | `14 MiB (7 instances)` | +| L3 cache | `18 MiB (1 instance)` | +| NUMA nodes | `1` | +| RAM total | `32260652 kB` | +| RAM available | `20372096 kB` | + +## satoru (225x225) + +![satoru](./parallel_satoru.png) + +| Потоки | cols, мс | raw, мс | pixel, мс | grid, мс | +|---:|---:|---:|---:|---:| +| 1 | `43.37 +- 0.38` | `43.59 +- 0.73` | `43.57 +- 0.15` | `43.09 +- 0.33` | +| 2 | `22.62 +- 1.94` | `22.18 +- 0.67` | `22.27 +- 1.12` | `24.55 +- 0.12` | +| 4 | `14.42 +- 3.67` | `12.49 +- 2.04` | `12.09 +- 1.60` | `13.31 +- 2.66` | +| 6 | `9.60 +- 0.25` | `9.79 +- 1.84` | `9.78 +- 1.01` | `13.20 +- 1.78` | +| 8 | `8.16 +- 1.82` | `9.25 +- 1.90` | `7.22 +- 0.15` | `8.47 +- 1.68` | +| 12 | `7.43 +- 0.50` | `6.92 +- 0.72` | `6.64 +- 0.83` | `8.53 +- 1.27` | +| 18 | `7.98 +- 1.24` | `7.79 +- 2.62` | `8.49 +- 3.48` | `8.38 +- 2.60` | + +## stariy_bog (914x480) + +![stariy_bog](./parallel_stariy_bog.png) + +| Потоки | cols, мс | raw, мс | pixel, мс | grid, мс | +|---:|---:|---:|---:|---:| +| 1 | `373.85 +- 3.78` | `372.09 +- 3.85` | `370.68 +- 6.96` | `370.50 +- 5.22` | +| 2 | `190.60 +- 8.49` | `189.17 +- 5.25` | `189.72 +- 6.82` | `188.33 +- 2.71` | +| 4 | `98.97 +- 5.87` | `102.37 +- 13.26` | `113.59 +- 4.82` | `106.11 +- 7.73` | +| 6 | `83.98 +- 14.79` | `80.01 +- 4.78` | `78.79 +- 3.29` | `79.32 +- 6.62` | +| 8 | `62.61 +- 2.71` | `61.75 +- 3.37` | `62.20 +- 3.35` | `60.76 +- 3.55` | +| 12 | `48.47 +- 2.62` | `49.19 +- 5.56` | `46.12 +- 5.65` | `49.05 +- 2.84` | +| 18 | `47.23 +- 4.67` | `46.75 +- 2.56` | `45.48 +- 2.55` | `45.52 +- 4.35` | + +## musashi (2560x1440) + +![musashi](./parallel_musashi.png) + +| Потоки | cols, мс | raw, мс | pixel, мс | grid, мс | +|---:|---:|---:|---:|---:| +| 1 | `3145.74 +- 18.19` | `3099.84 +- 21.70` | `3170.92 +- 22.00` | `3143.83 +- 38.42` | +| 2 | `1595.28 +- 21.83` | `1585.85 +- 17.96` | `1595.82 +- 13.12` | `1588.32 +- 31.36` | +| 4 | `885.53 +- 64.09` | `847.94 +- 42.13` | `834.87 +- 25.18` | `805.13 +- 20.02` | +| 6 | `646.96 +- 12.23` | `646.58 +- 10.03` | `651.55 +- 8.69` | `644.20 +- 6.45` | +| 8 | `495.77 +- 4.10` | `498.54 +- 10.13` | `509.07 +- 14.84` | `499.93 +- 7.34` | +| 12 | `347.93 +- 10.72` | `355.26 +- 22.94` | `374.35 +- 59.09` | `435.61 +- 29.07` | +| 18 | `441.94 +- 97.90` | `425.78 +- 41.61` | `420.95 +- 12.05` | `412.47 +- 12.66` | + +## sea (3840x2160) + +![sea](./parallel_sea.png) + +| Потоки | cols, мс | raw, мс | pixel, мс | grid, мс | +|---:|---:|---:|---:|---:| +| 1 | `7094.83 +- 61.01` | `7077.91 +- 29.20` | `7151.32 +- 36.64` | `7048.17 +- 45.98` | +| 2 | `3544.69 +- 13.05` | `3564.77 +- 26.92` | `3590.63 +- 6.64` | `3575.58 +- 23.95` | +| 4 | `1831.73 +- 57.35` | `1907.29 +- 48.01` | `1912.65 +- 131.05` | `1827.61 +- 28.31` | +| 6 | `1444.55 +- 13.00` | `1437.96 +- 24.90` | `1442.13 +- 24.80` | `1473.82 +- 22.26` | +| 8 | `1135.15 +- 21.31` | `1128.03 +- 7.99` | `1138.94 +- 13.39` | `1137.57 +- 13.89` | +| 12 | `841.05 +- 22.13` | `837.51 +- 24.32` | `819.09 +- 25.53` | `812.81 +- 26.21` | +| 18 | `924.17 +- 133.78` | `882.68 +- 49.11` | `880.43 +- 32.01` | `858.09 +- 23.09` | diff --git a/docs/task2/parallel_musashi.png b/docs/task2/parallel_musashi.png new file mode 100644 index 0000000..484439a Binary files /dev/null and b/docs/task2/parallel_musashi.png differ diff --git a/docs/task2/parallel_satoru.png b/docs/task2/parallel_satoru.png new file mode 100644 index 0000000..5a4ca98 Binary files /dev/null and b/docs/task2/parallel_satoru.png differ diff --git a/docs/task2/parallel_sea.png b/docs/task2/parallel_sea.png new file mode 100644 index 0000000..0593829 Binary files /dev/null and b/docs/task2/parallel_sea.png differ diff --git a/docs/task2/parallel_stariy_bog.png b/docs/task2/parallel_stariy_bog.png new file mode 100644 index 0000000..4c3d765 Binary files /dev/null and b/docs/task2/parallel_stariy_bog.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..3fa53f2 --- /dev/null +++ b/src/app.c @@ -0,0 +1,125 @@ +#include "cli_args.h" + +#include +#include +#include + +#include +#include + +#include +#include + +#define MAX_ERROR_MESSAGE_LENGTH 32 + +static double elapsed_ms(const struct timespec *start, + const struct timespec *end) { + const double seconds = (double)(end->tv_sec - start->tv_sec) * 1000.0; + const double nanoseconds = + (double)(end->tv_nsec - start->tv_nsec) / 1000000.0; + + return seconds + nanoseconds; +} + +typedef int (*convolution_runner_t)(const filter_t *filter, + image_view_t *image_view); + +static convolution_runner_t select_convolution_runner(execution_mode_t mode) { + switch (mode) { + case EXECUTION_MODE_SEQ: + return sequential_convolution; + case EXECUTION_MODE_ROWS: + return parallel_convolution_rows; + case EXECUTION_MODE_PIXELS: + return parallel_convolution_pixels; + case EXECUTION_MODE_COLS: + return parallel_convolution_cols; + case EXECUTION_MODE_GRID: + return parallel_convolution_rectangle; + default: + return NULL; + } +} + +static int apply_filters(const cli_request_t *request, + image_view_t *image_view) { + convolution_runner_t run_convolution = + select_convolution_runner(request->mode); + if (run_convolution == NULL) { + return -1; + } + + 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) || + run_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, + }; + struct timespec start_time; + struct timespec end_time; + + timespec_get(&start_time, TIME_UTC); + + if (apply_filters(&request, &image_view) != 0) { + cvReleaseImage(&image); + fputs("failed to apply filters\n", stderr); + return -1; + } + + timespec_get(&end_time, TIME_UTC); + printf("processing time: %.3f ms\n", elapsed_ms(&start_time, &end_time)); + + 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/app/execution_config.h b/src/app/execution_config.h new file mode 100644 index 0000000..2bcd6f9 --- /dev/null +++ b/src/app/execution_config.h @@ -0,0 +1,30 @@ +#pragma once + +#include + +typedef enum execution_mode { + EXECUTION_MODE_SEQ = 0, + EXECUTION_MODE_ROWS, + EXECUTION_MODE_PIXELS, + EXECUTION_MODE_COLS, + EXECUTION_MODE_GRID, +} execution_mode_t; + +typedef struct execution_config { + execution_mode_t mode; + size_t threads; + size_t grid_rows; + size_t grid_cols; +} execution_config_t; + +static inline execution_config_t make_execution_config(execution_mode_t mode, + size_t threads, + size_t grid_rows, + size_t grid_cols) { + return (execution_config_t){ + .mode = mode, + .threads = threads, + .grid_rows = grid_rows, + .grid_cols = grid_cols, + }; +} diff --git a/src/cli_args.c b/src/cli_args.c new file mode 100644 index 0000000..e8dbc81 --- /dev/null +++ b/src/cli_args.c @@ -0,0 +1,230 @@ +#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 bool cli_parse_execution_mode(const char *text, execution_mode_t *mode) { + if (strcmp(text, "cols") == 0) { + *mode = EXECUTION_MODE_COLS; + } else if (strcmp(text, "rows") == 0 || strcmp(text, "raws") == 0) { + *mode = EXECUTION_MODE_ROWS; + } else if (strcmp(text, "pixels") == 0) { + *mode = EXECUTION_MODE_PIXELS; + } else if (strcmp(text, "grid") == 0 || strcmp(text, "rectangle") == 0 || + strcmp(text, "random") == 0) { + *mode = EXECUTION_MODE_GRID; + } else { + return false; + } + + return true; +} + +static void cli_init_request(cli_request_t *request) { + memset(request, 0, sizeof(*request)); + request->mode = EXECUTION_MODE_SEQ; + 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 (index >= argc) { + return cli_invalid(error_message, error_message_size); + } + + if (strcmp(argv[index], "-s") == 0 && index + 1 == argc) { + request->mode = EXECUTION_MODE_SEQ; + } else if (strcmp(argv[index], "-p") == 0 && index + 2 == argc) { + if (!cli_parse_execution_mode(argv[index + 1], &request->mode)) { + return cli_invalid(error_message, error_message_size); + } + } else { + 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 ] -p " + "\n" + " %s -i -o -f -h -w " + "[-t ] " + "-f -h -w [-t ] " + "(-s | -p )\n", + name, + name, + name); +} diff --git a/src/cli_args.h b/src/cli_args.h new file mode 100644 index 0000000..c96863e --- /dev/null +++ b/src/cli_args.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#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; + execution_mode_t mode; + 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/convolution/convolution.c b/src/convolution/convolution.c new file mode 100644 index 0000000..4b7bea9 --- /dev/null +++ b/src/convolution/convolution.c @@ -0,0 +1,163 @@ +#include "convolution.h" +#include + +#include +#include +#include + +static int validate_convolution_input(const filter_t *filter, + const 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; + } + + return 0; +} + +static int convolution_context_is_valid(const convolution_context_t *context) { + return context != NULL && context->filter != NULL && + context->source != NULL && context->destination != NULL && + context->width != 0U && context->height != 0U && + context->stride != 0U && context->channels != 0U; +} + +static void convolution_apply_pixel_unchecked( + const convolution_context_t *context, size_t y, size_t x) { + const filter_t *filter = context->filter; + unsigned char *dst_pixel = + context->destination + y * context->stride + x * context->channels; + + for (size_t channel = 0; channel < context->channels; ++channel) { + if (context->channels == 4U && channel == 3U) { + dst_pixel[channel] = + context->source[y * context->stride + x * context->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, context->width, filter->border_mode); + const size_t image_y = + resolve_index(src_y, context->height, filter->border_mode); + + const unsigned char *src_pixel = context->source + + image_y * context->stride + + image_x * context->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); + } +} + +int convolution_run(const filter_t *filter, + image_view_t *image_view, + convolution_executor_t executor) { + if (executor == NULL || validate_convolution_input(filter, image_view) != 0) { + return -1; + } + + const size_t buffer_size = image_view->height * image_view->stride; + unsigned char *source_copy = (unsigned char *)malloc(buffer_size); + if (source_copy == NULL) { + return -1; + } + + memcpy(source_copy, image_view->data, buffer_size); + + const convolution_context_t context = { + .filter = filter, + .source = source_copy, + .destination = image_view->data, + .height = image_view->height, + .width = image_view->width, + .stride = image_view->stride, + .channels = image_view->channels, + }; + + const int result = executor(&context); + free(source_copy); + return result; +} + +int convolution_apply_pixel(const convolution_context_t *context, + size_t y, + size_t x) { + if (!convolution_context_is_valid(context) || y >= context->height || + x >= context->width) { + return -1; + } + + convolution_apply_pixel_unchecked(context, y, x); + return 0; +} + +int convolution_apply_rect(const convolution_context_t *context, + size_t y_begin, + size_t y_end, + size_t x_begin, + size_t x_end) { + if (!convolution_context_is_valid(context) || y_begin > y_end || + x_begin > x_end || y_end > context->height || x_end > context->width) { + return -1; + } + + for (size_t y = y_begin; y < y_end; ++y) { + for (size_t x = x_begin; x < x_end; ++x) { + convolution_apply_pixel_unchecked(context, y, x); + } + } + + return 0; +} + +int convolution_apply_rows(const convolution_context_t *context, + size_t y_begin, + size_t y_end) { + if (!convolution_context_is_valid(context)) { + return -1; + } + + return convolution_apply_rect(context, y_begin, y_end, 0U, context->width); +} + +int convolution_apply_cols(const convolution_context_t *context, + size_t x_begin, + size_t x_end) { + if (!convolution_context_is_valid(context)) { + return -1; + } + + return convolution_apply_rect(context, 0U, context->height, x_begin, x_end); +} diff --git a/src/convolution/convolution.h b/src/convolution/convolution.h new file mode 100644 index 0000000..f8689dd --- /dev/null +++ b/src/convolution/convolution.h @@ -0,0 +1,40 @@ +#pragma once + +#include + +#include +#include + +typedef struct convolution_context { + const filter_t *filter; + const unsigned char *source; + unsigned char *destination; + size_t height; + size_t width; + size_t stride; + size_t channels; +} convolution_context_t; + +typedef int (*convolution_executor_t)(const convolution_context_t *context); + +int convolution_run(const filter_t *filter, + image_view_t *image_view, + convolution_executor_t executor); + +int convolution_apply_pixel(const convolution_context_t *context, + size_t y, + size_t x); + +int convolution_apply_rect(const convolution_context_t *context, + size_t y_begin, + size_t y_end, + size_t x_begin, + size_t x_end); + +int convolution_apply_rows(const convolution_context_t *context, + size_t y_begin, + size_t y_end); + +int convolution_apply_cols(const convolution_context_t *context, + size_t x_begin, + size_t x_end); 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/parallel_convolution/parallel_convolution.c b/src/parallel_convolution/parallel_convolution.c new file mode 100644 index 0000000..e90840b --- /dev/null +++ b/src/parallel_convolution/parallel_convolution.c @@ -0,0 +1,109 @@ +#include "parallel_convolution.h" + +#include +#include + +#define TILE_HEIGHT 32U +#define TILE_WIDTH 128U + +static size_t min_size(size_t lhs, size_t rhs) { + return lhs < rhs ? lhs : rhs; +} + +static int rows_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel reduction(| : failed) + { + const size_t thread_index = (size_t)omp_get_thread_num(); + const size_t thread_count = (size_t)omp_get_num_threads(); + + const size_t first_row = thread_index * context->height / thread_count; + const size_t last_row = + (thread_index + 1U) * context->height / thread_count; + + if (convolution_apply_rows(context, first_row, last_row) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int cols_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel reduction(| : failed) + { + const size_t thread_index = (size_t)omp_get_thread_num(); + const size_t thread_count = (size_t)omp_get_num_threads(); + + const size_t first_col = thread_index * context->width / thread_count; + const size_t last_col = (thread_index + 1U) * context->width / thread_count; + + if (convolution_apply_cols(context, first_col, last_col) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int pixels_executor(const convolution_context_t *context) { + int failed = 0; + const size_t pixel_count = context->height * context->width; + +#pragma omp parallel for schedule(static) reduction(| : failed) + for (size_t pixel_index = 0; pixel_index < pixel_count; ++pixel_index) { + const size_t y = pixel_index / context->width; + const size_t x = pixel_index % context->width; + + if (convolution_apply_pixel(context, y, x) != 0) { + failed = 1; + } + } + + return failed ? -1 : 0; +} + +static int tiles_executor(const convolution_context_t *context) { + int failed = 0; + +#pragma omp parallel for collapse(2) schedule(static) reduction(| : failed) + for (size_t first_row = 0; first_row < context->height; + first_row += TILE_HEIGHT) { + for (size_t first_col = 0; first_col < context->width; + first_col += TILE_WIDTH) { + const size_t last_row = + min_size(first_row + TILE_HEIGHT, context->height); + const size_t last_col = min_size(first_col + TILE_WIDTH, context->width); + + if (convolution_apply_rect( + context, first_row, last_row, first_col, last_col) != 0) { + failed = 1; + } + } + } + + return failed ? -1 : 0; +} + +int parallel_convolution_rows(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, rows_executor); +} + +int parallel_convolution_cols(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, cols_executor); +} + +int parallel_convolution_pixels(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, pixels_executor); +} + +int parallel_convolution_rectangle(const filter_t *filter, + image_view_t *image_view) { + return convolution_run(filter, image_view, tiles_executor); +} diff --git a/src/parallel_convolution/parallel_convolution.h b/src/parallel_convolution/parallel_convolution.h new file mode 100644 index 0000000..68c994b --- /dev/null +++ b/src/parallel_convolution/parallel_convolution.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include +#include + +int parallel_convolution_rows(const filter_t *filter, image_view_t *image_view); + +int parallel_convolution_cols(const filter_t *filter, image_view_t *image_view); + +int parallel_convolution_pixels(const filter_t *filter, + image_view_t *image_view); + +int parallel_convolution_rectangle(const filter_t *filter, + image_view_t *image_view); diff --git a/src/sequentially_convolution/sequentially_convolution.c b/src/sequentially_convolution/sequentially_convolution.c new file mode 100644 index 0000000..2f5ca09 --- /dev/null +++ b/src/sequentially_convolution/sequentially_convolution.c @@ -0,0 +1,12 @@ +#include "sequentially_convolution.h" + +#include + +static int sequential_executor(const convolution_context_t *context) { + return convolution_apply_rect( + context, 0U, context->height, 0U, context->width); +} + +int sequential_convolution(const filter_t *filter, image_view_t *image_view) { + return convolution_run(filter, image_view, sequential_executor); +} 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); +}